├── index.js ├── .gitignore ├── README.md ├── gulpfile.js ├── nodes └── Odoo │ ├── Odoo.node.json │ ├── odoo.svg │ ├── ResourceDescription.ts │ ├── Odoo.node.ts │ └── GenericFunctions.ts ├── CONTRIBUTOR_LICENSE_AGREEMENT.md ├── tsconfig.json ├── LICENSE.md ├── .prettierrc.js ├── package.json ├── .eslintrc.js └── tslint.json /index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .tmp 4 | tmp 5 | dist 6 | npm-debug.log* 7 | package-lock.json 8 | yarn.lock 9 | .idea 10 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # n8n-nodes-odoo 2 | 3 | ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png) 4 | 5 | [Click here to view the README](https://digital-boss.de/n8n/?packageName=n8n-nodes-odoo) -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { task, src, dest } = require('gulp'); 3 | 4 | task('build:icons', copyIcons); 5 | 6 | function copyIcons() { 7 | const nodeSource = path.resolve('nodes', '**', '*.{png,svg}'); 8 | const nodeDestination = path.resolve('dist', 'nodes'); 9 | 10 | return src(nodeSource).pipe(dest(nodeDestination)); 11 | } 12 | -------------------------------------------------------------------------------- /nodes/Odoo/Odoo.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-base.odoo", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Data & Storage"], 6 | "resources": { 7 | "credentialDocumentation": [ 8 | { 9 | "url": "https://docs.n8n.io/credentials/odoo" 10 | } 11 | ], 12 | "primaryDocumentation": [ 13 | { 14 | "url": "https://docs.n8n.io/nodes/n8n-nodes-base.odoo/" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CONTRIBUTOR_LICENSE_AGREEMENT.md: -------------------------------------------------------------------------------- 1 | # n8n Contributor License Agreement 2 | 3 | I give n8n permission to license my contributions on any terms they like. I am giving them this license in order to make it possible for them to accept my contributions into their project. 4 | 5 | **_As far as the law allows, my contributions come as is, without any warranty or condition, and I will not be liable to anyone for any damages related to this software or this license, under any kind of legal claim._** 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "lib": ["es2019", "es2020", "es2022.error", "dom"], 8 | "removeComments": true, 9 | "useUnknownInCatchVariables": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "preserveConstEnums": true, 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "incremental": true, 19 | "declaration": true, 20 | "sourceMap": true, 21 | "skipLibCheck": true, 22 | "outDir": "./dist/" 23 | }, 24 | "include": ["nodes/**/*", "nodes/**/*.json", "package.json"] 25 | } 26 | -------------------------------------------------------------------------------- /nodes/Odoo/odoo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 n8n GmbH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * https://prettier.io/docs/en/options.html#semicolons 4 | */ 5 | semi: true, 6 | 7 | /** 8 | * https://prettier.io/docs/en/options.html#trailing-commas 9 | */ 10 | trailingComma: 'all', 11 | 12 | /** 13 | * https://prettier.io/docs/en/options.html#bracket-spacing 14 | */ 15 | bracketSpacing: true, 16 | 17 | /** 18 | * https://prettier.io/docs/en/options.html#tabs 19 | */ 20 | useTabs: true, 21 | 22 | /** 23 | * https://prettier.io/docs/en/options.html#tab-width 24 | */ 25 | tabWidth: 2, 26 | 27 | /** 28 | * https://prettier.io/docs/en/options.html#arrow-function-parentheses 29 | */ 30 | arrowParens: 'always', 31 | 32 | /** 33 | * https://prettier.io/docs/en/options.html#quotes 34 | */ 35 | singleQuote: true, 36 | 37 | /** 38 | * https://prettier.io/docs/en/options.html#quote-props 39 | */ 40 | quoteProps: 'as-needed', 41 | 42 | /** 43 | * https://prettier.io/docs/en/options.html#end-of-line 44 | */ 45 | endOfLine: 'lf', 46 | 47 | /** 48 | * https://prettier.io/docs/en/options.html#print-width 49 | */ 50 | printWidth: 100, 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@digital-boss/n8n-nodes-odoo", 3 | "version": "0.5.0", 4 | "description": "A node consuming the Odoo API.", 5 | "keywords": [ 6 | "n8n", 7 | "workflow", 8 | "n8n-community-node-package", 9 | "digital boss", 10 | "odoo" 11 | ], 12 | "license": "MIT", 13 | "homepage": "https://n8n.io", 14 | "author": { 15 | "name": "Steffen Hannesschlaeger", 16 | "email": "steffen_hannesschlaeger@hotmail.de" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/digital-boss/n8n-nodes-odoo" 21 | }, 22 | "main": "index.js", 23 | "scripts": { 24 | "build": "tsc && gulp build:icons", 25 | "dev": "tsc --watch", 26 | "format": "prettier nodes --write", 27 | "lint": "eslint nodes package.json", 28 | "lintfix": "eslint nodes package.json --fix", 29 | "prepublishOnly": "npm run build && npm run lint -c .eslintrc.prepublish.js nodes package.json" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "n8n": { 35 | "n8nNodesApiVersion": 1, 36 | "nodes": [ 37 | "dist/nodes/Odoo/Odoo.node.js" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@types/express": "^4.17.6", 42 | "@types/request-promise-native": "~1.0.15", 43 | "@typescript-eslint/parser": "^5.54.0", 44 | "eslint-plugin-n8n-nodes-base": "^1.12.1", 45 | "gulp": "^4.0.2", 46 | "n8n-core": "*", 47 | "n8n-workflow": "*", 48 | "nodelinter": "^0.1.19", 49 | "prettier": "^2.8.4", 50 | "typescript": "~4.8.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | root: true, 6 | 7 | env: { 8 | browser: true, 9 | es6: true, 10 | node: true, 11 | }, 12 | 13 | parser: '@typescript-eslint/parser', 14 | 15 | parserOptions: { 16 | project: ['./tsconfig.json'], 17 | sourceType: 'module', 18 | extraFileExtensions: ['.json'], 19 | }, 20 | 21 | ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'], 22 | 23 | overrides: [ 24 | { 25 | files: ['package.json'], 26 | plugins: ['eslint-plugin-n8n-nodes-base'], 27 | extends: ['plugin:n8n-nodes-base/community'], 28 | rules: { 29 | 'n8n-nodes-base/community-package-json-name-still-default': 'off', 30 | }, 31 | }, 32 | { 33 | files: ['./credentials/**/*.ts'], 34 | plugins: ['eslint-plugin-n8n-nodes-base'], 35 | extends: ['plugin:n8n-nodes-base/credentials'], 36 | rules: { 37 | 'n8n-nodes-base/cred-class-field-documentation-url-missing': 'off', 38 | 'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off', 39 | }, 40 | }, 41 | { 42 | files: ['./nodes/**/*.ts'], 43 | plugins: ['eslint-plugin-n8n-nodes-base'], 44 | extends: ['plugin:n8n-nodes-base/nodes'], 45 | rules: { 46 | 'n8n-nodes-base/node-execute-block-missing-continue-on-fail': 'off', 47 | 'n8n-nodes-base/node-resource-description-filename-against-convention': 'off', 48 | 'n8n-nodes-base/node-param-fixed-collection-type-unsorted-items': 'off', 49 | 'n8n-nodes-base/node-param-description-missing-from-dynamic-options': 'off', 50 | 'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options': 'off', 51 | 'n8n-nodes-base/node-param-type-options-max-value-present': 'off', 52 | 'n8n-nodes-base/node-param-description-wrong-for-dynamic-options': 'off', 53 | 'n8n-nodes-base/node-param-description-missing-from-dynamic-multi-options': 'off', 54 | 'n8n-nodes-base/node-param-resource-without-no-data-expression': 'off', 55 | 'n8n-nodes-base/node-param-operation-without-no-data-expression': 'off', 56 | }, 57 | }, 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "linterOptions": { 3 | "exclude": ["node_modules/**/*"] 4 | }, 5 | "defaultSeverity": "error", 6 | "jsRules": {}, 7 | "rules": { 8 | "array-type": [true, "array-simple"], 9 | "arrow-return-shorthand": true, 10 | "ban": [ 11 | true, 12 | { 13 | "name": "Array", 14 | "message": "tsstyle#array-constructor" 15 | } 16 | ], 17 | "ban-types": [ 18 | true, 19 | ["Object", "Use {} instead."], 20 | ["String", "Use 'string' instead."], 21 | ["Number", "Use 'number' instead."], 22 | ["Boolean", "Use 'boolean' instead."] 23 | ], 24 | "class-name": true, 25 | "curly": [true, "ignore-same-line"], 26 | "forin": true, 27 | "jsdoc-format": true, 28 | "label-position": true, 29 | "indent": [true, "tabs", 2], 30 | "member-access": [true, "no-public"], 31 | "new-parens": true, 32 | "no-angle-bracket-type-assertion": true, 33 | "no-any": true, 34 | "no-arg": true, 35 | "no-conditional-assignment": true, 36 | "no-construct": true, 37 | "no-debugger": true, 38 | "no-default-export": true, 39 | "no-duplicate-variable": true, 40 | "no-inferrable-types": true, 41 | "ordered-imports": [ 42 | true, 43 | { 44 | "import-sources-order": "any", 45 | "named-imports-order": "case-insensitive" 46 | } 47 | ], 48 | "no-namespace": [true, "allow-declarations"], 49 | "no-reference": true, 50 | "no-string-throw": true, 51 | "no-unused-expression": true, 52 | "no-var-keyword": true, 53 | "object-literal-shorthand": true, 54 | "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], 55 | "prefer-const": true, 56 | "radix": true, 57 | "semicolon": [true, "always", "ignore-bound-class-methods"], 58 | "switch-default": true, 59 | "trailing-comma": [ 60 | true, 61 | { 62 | "multiline": { 63 | "objects": "always", 64 | "arrays": "always", 65 | "functions": "always", 66 | "typeLiterals": "ignore" 67 | }, 68 | "esSpecCompliant": true 69 | } 70 | ], 71 | "triple-equals": [true, "allow-null-check"], 72 | "use-isnan": true, 73 | "quotes": ["error", "single"], 74 | "variable-name": [ 75 | true, 76 | "check-format", 77 | "ban-keywords", 78 | "allow-leading-underscore", 79 | "allow-trailing-underscore" 80 | ] 81 | }, 82 | "rulesDirectory": [] 83 | } 84 | -------------------------------------------------------------------------------- /nodes/Odoo/ResourceDescription.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const resourceOperations: INodeProperties[] = [ 4 | { 5 | displayName: 'Resource', 6 | name: 'resource', 7 | type: 'options', 8 | default: '', 9 | description: 'Choose from the list', 10 | typeOptions: { 11 | loadOptionsMethod: 'getModels', 12 | }, 13 | }, 14 | { 15 | displayName: 'Operation', 16 | name: 'operation', 17 | type: 'options', 18 | default: '', 19 | description: 'Choose from the list', 20 | typeOptions: { 21 | loadOptionsMethod: 'getOperations', 22 | }, 23 | }, 24 | { 25 | displayName: 'Custom Operation', 26 | name: 'customOperation', 27 | type: 'options', 28 | default: '', 29 | description: 30 | 'Trigger any actionable method associated with the resource, such as action_confirm', 31 | required: true, 32 | typeOptions: { 33 | loadOptionsDependsOn: ['resource'], 34 | loadOptionsMethod: 'getActions', 35 | }, 36 | displayOptions: { 37 | show: { 38 | operation: ['workflow'], 39 | }, 40 | }, 41 | }, 42 | ]; 43 | 44 | export const resourceDescription: INodeProperties[] = [ 45 | /* -------------------------------------------------------------------------- */ 46 | /* custom:create */ 47 | /* -------------------------------------------------------------------------- */ 48 | { 49 | displayName: 'Fields', 50 | name: 'fieldsToCreateOrUpdate', 51 | type: 'fixedCollection', 52 | typeOptions: { 53 | multipleValues: true, 54 | multipleValueButtonText: 'Add Field', 55 | }, 56 | default: {}, 57 | placeholder: 'Add Field', 58 | displayOptions: { 59 | show: { 60 | operation: ['create'], 61 | }, 62 | }, 63 | options: [ 64 | { 65 | displayName: 'Field Record:', 66 | name: 'fields', 67 | values: [ 68 | { 69 | displayName: 'Field Name', 70 | name: 'fieldName', 71 | type: 'options', 72 | default: '', 73 | typeOptions: { 74 | loadOptionsMethod: 'getModelFields', 75 | }, 76 | }, 77 | { 78 | displayName: 'New Value', 79 | name: 'fieldValue', 80 | type: 'string', 81 | default: '', 82 | }, 83 | ], 84 | }, 85 | ], 86 | }, 87 | 88 | /* -------------------------------------------------------------------------- */ 89 | /* custom:get */ 90 | /* -------------------------------------------------------------------------- */ 91 | { 92 | displayName: 'ID', 93 | name: 'id', 94 | type: 'string', 95 | description: 'The resource ID', 96 | default: '', 97 | required: true, 98 | displayOptions: { 99 | show: { 100 | operation: ['get', 'delete'], 101 | }, 102 | }, 103 | }, 104 | /* -------------------------------------------------------------------------- */ 105 | /* custom:getAll */ 106 | /* -------------------------------------------------------------------------- */ 107 | { 108 | displayName: 'Return All', 109 | name: 'returnAll', 110 | type: 'boolean', 111 | displayOptions: { 112 | show: { 113 | operation: ['getAll'], 114 | }, 115 | }, 116 | default: false, 117 | description: 'Whether to return all results or only up to a given limit', 118 | }, 119 | 120 | { 121 | displayName: 'Offset', 122 | name: 'offset', 123 | type: 'number', 124 | default: 0, 125 | displayOptions: { 126 | show: { 127 | operation: ['getAll'], 128 | returnAll: [false], 129 | }, 130 | }, 131 | typeOptions: { 132 | minValue: 0, 133 | }, 134 | description: 'Number of results to skip', 135 | }, 136 | { 137 | displayName: 'Limit', 138 | name: 'limit', 139 | type: 'number', 140 | default: 50, 141 | displayOptions: { 142 | show: { 143 | operation: ['getAll'], 144 | returnAll: [false], 145 | }, 146 | }, 147 | typeOptions: { 148 | minValue: 1, 149 | maxValue: 1000, 150 | }, 151 | description: 'Max number of results to return', 152 | }, 153 | { 154 | displayName: 'Options', 155 | name: 'options', 156 | type: 'collection', 157 | default: {}, 158 | placeholder: 'Add Field', 159 | displayOptions: { 160 | show: { 161 | operation: ['getAll', 'get'], 162 | }, 163 | }, 164 | options: [ 165 | { 166 | displayName: 'Fields To Include', 167 | name: 'fieldsList', 168 | type: 'multiOptions', 169 | default: [], 170 | typeOptions: { 171 | loadOptionsMethod: 'getModelFields', 172 | loadOptionsDependsOn: ['resource'], 173 | }, 174 | }, 175 | ], 176 | }, 177 | { 178 | displayName: 'Filters', 179 | name: 'filterRequest', 180 | type: 'fixedCollection', 181 | typeOptions: { 182 | multipleValues: true, 183 | multipleValueButtonText: 'Add Filter', 184 | }, 185 | default: {}, 186 | description: 'Filter request by applying filters', 187 | placeholder: 'Add condition', 188 | displayOptions: { 189 | show: { 190 | operation: ['getAll'], 191 | }, 192 | }, 193 | options: [ 194 | { 195 | name: 'filter', 196 | displayName: 'Filter', 197 | values: [ 198 | { 199 | displayName: 'Field', 200 | name: 'fieldName', 201 | type: 'options', 202 | default: '', 203 | typeOptions: { 204 | loadOptionsDependsOn: ['resource'], 205 | loadOptionsMethod: 'getModelFields', 206 | }, 207 | }, 208 | { 209 | displayName: 'Operator', 210 | name: 'operator', 211 | type: 'options', 212 | default: 'equal', 213 | description: 'Specify an operator', 214 | options: [ 215 | { 216 | name: '!=', 217 | value: 'notEqual', 218 | }, 219 | { 220 | name: '<', 221 | value: 'lesserThen', 222 | }, 223 | { 224 | name: '<=', 225 | value: 'lesserOrEqual', 226 | }, 227 | { 228 | name: '=', 229 | value: 'equal', 230 | }, 231 | { 232 | name: '>', 233 | value: 'greaterThen', 234 | }, 235 | { 236 | name: '>=', 237 | value: 'greaterOrEqual', 238 | }, 239 | { 240 | name: 'Child Of', 241 | value: 'childOf', 242 | }, 243 | { 244 | name: 'In', 245 | value: 'in', 246 | }, 247 | { 248 | name: 'Like', 249 | value: 'like', 250 | }, 251 | { 252 | name: 'Not In', 253 | value: 'notIn', 254 | }, 255 | ], 256 | }, 257 | { 258 | displayName: 'Value', 259 | name: 'value', 260 | type: 'string', 261 | default: '', 262 | description: 'Specify value for comparison', 263 | }, 264 | ], 265 | }, 266 | ], 267 | }, 268 | /* -------------------------------------------------------------------------- */ 269 | /* custom:update */ 270 | /* -------------------------------------------------------------------------- */ 271 | { 272 | displayName: 'ID', 273 | name: 'id', 274 | type: 'string', 275 | description: 'The resource ID', 276 | default: '', 277 | required: true, 278 | 279 | displayOptions: { 280 | show: { 281 | operation: ['update'], 282 | }, 283 | }, 284 | }, 285 | 286 | { 287 | displayName: 'Update Fields', 288 | name: 'fieldsToCreateOrUpdate', 289 | type: 'fixedCollection', 290 | typeOptions: { 291 | multipleValues: true, 292 | multipleValueButtonText: 'Add Field', 293 | }, 294 | default: {}, 295 | placeholder: 'Add Field', 296 | displayOptions: { 297 | show: { 298 | operation: ['update'], 299 | }, 300 | }, 301 | options: [ 302 | { 303 | displayName: 'Field Record:', 304 | name: 'fields', 305 | values: [ 306 | { 307 | displayName: 'Field Name', 308 | name: 'fieldName', 309 | type: 'options', 310 | default: '', 311 | typeOptions: { 312 | loadOptionsMethod: 'getModelFields', 313 | }, 314 | }, 315 | { 316 | displayName: 'New Value', 317 | name: 'fieldValue', 318 | type: 'string', 319 | default: '', 320 | }, 321 | ], 322 | }, 323 | ], 324 | }, 325 | 326 | /* -------------------------------------------------------------------------- */ 327 | /* custom:workflow */ 328 | /* -------------------------------------------------------------------------- */ 329 | { 330 | displayName: 'ID', 331 | name: 'id', 332 | type: 'string', 333 | description: 'The resource ID', 334 | default: '', 335 | required: true, 336 | displayOptions: { 337 | show: { 338 | operation: ['workflow'], 339 | }, 340 | }, 341 | }, 342 | ]; 343 | -------------------------------------------------------------------------------- /nodes/Odoo/Odoo.node.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions } from 'n8n-core'; 2 | 3 | import { 4 | IDataObject, 5 | ILoadOptionsFunctions, 6 | INodeExecutionData, 7 | INodePropertyOptions, 8 | INodeType, 9 | INodeTypeDescription, 10 | JsonObject, 11 | } from 'n8n-workflow'; 12 | 13 | import { 14 | IOdooFilterOperations, 15 | odooCreate, 16 | odooDelete, 17 | odooGet, 18 | odooGetActionMethods, 19 | odooGetAll, 20 | odooGetDBName, 21 | odooGetModelFields, 22 | odooGetUserID, 23 | odooIsAddonInstalled, 24 | odooJSONRPCRequest, 25 | odooUpdate, 26 | odooWorkflow, 27 | processNameValueFields, 28 | } from './GenericFunctions'; 29 | import { resourceDescription, resourceOperations } from './ResourceDescription'; 30 | 31 | export class Odoo implements INodeType { 32 | description: INodeTypeDescription = { 33 | displayName: 'Odoo', 34 | name: 'odoo', 35 | icon: 'file:odoo.svg', 36 | group: ['transform'], 37 | version: 1, 38 | description: 'Consume Odoo API', 39 | subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', 40 | defaults: { 41 | name: 'Odoo', 42 | }, 43 | inputs: ['main'], 44 | outputs: ['main'], 45 | credentials: [ 46 | { 47 | name: 'odooApi', 48 | required: true, 49 | testedBy: 'odooApiTest', 50 | }, 51 | ], 52 | properties: [...resourceOperations, ...resourceDescription], 53 | }; 54 | 55 | methods = { 56 | loadOptions: { 57 | async getModelFields(this: ILoadOptionsFunctions): Promise { 58 | let resource; 59 | resource = this.getCurrentNodeParameter('resource') as string; 60 | if (!resource) { 61 | return []; 62 | } 63 | 64 | const credentials = await this.getCredentials('odooApi'); 65 | const url = credentials?.url as string; 66 | const username = credentials?.username as string; 67 | const password = credentials?.password as string; 68 | const db = odooGetDBName(credentials?.db as string, url); 69 | const userID = await odooGetUserID.call(this, db, username, password, url); 70 | 71 | const response = await odooGetModelFields.call(this, db, userID, password, resource, url); 72 | 73 | const options = Object.entries(response).map(([k, v]) => { 74 | const optionField = v as { [key: string]: string }; 75 | return { 76 | name: optionField.string, 77 | value: k, 78 | // nodelinter-ignore-next-line 79 | description: `name: ${optionField?.string}, type: ${optionField?.type} required: ${optionField?.required}`, 80 | }; 81 | }); 82 | 83 | return options.sort((a, b) => a.name?.localeCompare(b.name) || 0); 84 | }, 85 | async getModels(this: ILoadOptionsFunctions): Promise { 86 | const credentials = await this.getCredentials('odooApi'); 87 | const url = credentials?.url as string; 88 | const username = credentials?.username as string; 89 | const password = credentials?.password as string; 90 | const db = odooGetDBName(credentials?.db as string, url); 91 | const userID = await odooGetUserID.call(this, db, username, password, url); 92 | 93 | const body = { 94 | jsonrpc: '2.0', 95 | method: 'call', 96 | params: { 97 | service: 'object', 98 | method: 'execute', 99 | args: [ 100 | db, 101 | userID, 102 | password, 103 | 'ir.model', 104 | 'search_read', 105 | [], 106 | ['name', 'model', 'modules'], 107 | ], 108 | }, 109 | id: Math.floor(Math.random() * 100), 110 | }; 111 | 112 | const response = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; 113 | 114 | const options = response.map((model) => ({ 115 | name: model.name, 116 | value: model.model, 117 | // eslint-disable-next-line n8n-nodes-base/node-param-description-line-break-html-tag 118 | description: `Model: ${model.model}
Modules: ${model.modules}`, 119 | })); 120 | return options as INodePropertyOptions[]; 121 | }, 122 | async getStates(this: ILoadOptionsFunctions): Promise { 123 | const credentials = await this.getCredentials('odooApi'); 124 | const url = credentials?.url as string; 125 | const username = credentials?.username as string; 126 | const password = credentials?.password as string; 127 | const db = odooGetDBName(credentials?.db as string, url); 128 | const userID = await odooGetUserID.call(this, db, username, password, url); 129 | 130 | const body = { 131 | jsonrpc: '2.0', 132 | method: 'call', 133 | params: { 134 | service: 'object', 135 | method: 'execute', 136 | args: [db, userID, password, 'res.country.state', 'search_read', [], ['id', 'name']], 137 | }, 138 | id: Math.floor(Math.random() * 100), 139 | }; 140 | 141 | const response = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; 142 | 143 | const options = response.map((state) => ({ 144 | name: state.name as string, 145 | value: state.id, 146 | })); 147 | return options.sort((a, b) => a.name?.localeCompare(b.name) || 0) as INodePropertyOptions[]; 148 | }, 149 | async getCountries(this: ILoadOptionsFunctions): Promise { 150 | const credentials = await this.getCredentials('odooApi'); 151 | const url = credentials?.url as string; 152 | const username = credentials?.username as string; 153 | const password = credentials?.password as string; 154 | const db = odooGetDBName(credentials?.db as string, url); 155 | const userID = await odooGetUserID.call(this, db, username, password, url); 156 | 157 | const body = { 158 | jsonrpc: '2.0', 159 | method: 'call', 160 | params: { 161 | service: 'object', 162 | method: 'execute', 163 | args: [db, userID, password, 'res.country', 'search_read', [], ['id', 'name']], 164 | }, 165 | id: Math.floor(Math.random() * 100), 166 | }; 167 | 168 | const response = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; 169 | 170 | const options = response.map((country) => ({ 171 | name: country.name as string, 172 | value: country.id, 173 | })); 174 | 175 | return options.sort((a, b) => a.name?.localeCompare(b.name) || 0) as INodePropertyOptions[]; 176 | }, 177 | async getActions(this: ILoadOptionsFunctions): Promise { 178 | let resource; 179 | resource = this.getCurrentNodeParameter('resource') as string; 180 | if (!resource) { 181 | return []; 182 | } 183 | 184 | const credentials = await this.getCredentials('odooApi'); 185 | const url = credentials?.url as string; 186 | const username = credentials?.username as string; 187 | const password = credentials?.password as string; 188 | const db = odooGetDBName(credentials?.db as string, url); 189 | const userID = await odooGetUserID.call(this, db, username, password, url); 190 | 191 | const response = await odooGetActionMethods.call(this, db, userID, password, resource, url); 192 | 193 | if (response) { 194 | const options = response.map((x) => ({ 195 | name: x, 196 | value: x, 197 | })); 198 | 199 | return options; 200 | } else { 201 | return []; 202 | } 203 | }, 204 | async getOperations(this: ILoadOptionsFunctions): Promise { 205 | const operations = [ 206 | { 207 | name: 'Create', 208 | value: 'create', 209 | description: 'Create a new item', 210 | }, 211 | { 212 | name: 'Delete', 213 | value: 'delete', 214 | description: 'Delete an item', 215 | }, 216 | { 217 | name: 'Get', 218 | value: 'get', 219 | description: 'Get an item', 220 | }, 221 | { 222 | name: 'Get Many', 223 | value: 'getAll', 224 | description: 'Get all items', 225 | }, 226 | { 227 | name: 'Update', 228 | value: 'update', 229 | description: 'Update an item', 230 | }, 231 | ]; 232 | 233 | const installed = await odooIsAddonInstalled.call(this); 234 | 235 | if (installed) { 236 | operations.push({ 237 | name: 'Workflow', 238 | value: 'workflow', 239 | description: 'Trigger a workflow action', 240 | }); 241 | } 242 | 243 | return operations; 244 | }, 245 | }, 246 | }; 247 | 248 | async execute(this: IExecuteFunctions): Promise { 249 | let items = this.getInputData(); 250 | items = JSON.parse(JSON.stringify(items)); 251 | const returnData: IDataObject[] = []; 252 | let responseData; 253 | 254 | const resource = this.getNodeParameter('resource', 0) as string; 255 | const operation = this.getNodeParameter('operation', 0) as string; 256 | 257 | const credentials = await this.getCredentials('odooApi'); 258 | const url = (credentials?.url as string).replace(/\/$/, ''); 259 | const username = credentials?.username as string; 260 | const password = credentials?.password as string; 261 | const db = odooGetDBName(credentials?.db as string, url); 262 | const userID = await odooGetUserID.call(this, db, username, password, url); 263 | 264 | //---------------------------------------------------------------------- 265 | // Main loop 266 | //---------------------------------------------------------------------- 267 | 268 | for (let i = 0; i < items.length; i++) { 269 | try { 270 | if (operation === 'create') { 271 | const fields = this.getNodeParameter('fieldsToCreateOrUpdate', i) as IDataObject; 272 | responseData = await odooCreate.call( 273 | this, 274 | db, 275 | userID, 276 | password, 277 | resource, 278 | operation, 279 | url, 280 | processNameValueFields(fields), 281 | ); 282 | } 283 | 284 | if (operation === 'delete') { 285 | const id = this.getNodeParameter('id', i) as string; 286 | responseData = await odooDelete.call( 287 | this, 288 | db, 289 | userID, 290 | password, 291 | resource, 292 | operation, 293 | url, 294 | id, 295 | ); 296 | } 297 | 298 | if (operation === 'get') { 299 | const id = this.getNodeParameter('id', i) as string; 300 | const options = this.getNodeParameter('options', i) as IDataObject; 301 | const fields = (options.fieldsList as IDataObject[]) || []; 302 | responseData = await odooGet.call( 303 | this, 304 | db, 305 | userID, 306 | password, 307 | resource, 308 | operation, 309 | url, 310 | id, 311 | fields, 312 | ); 313 | } 314 | 315 | if (operation === 'getAll') { 316 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 317 | const options = this.getNodeParameter('options', i) as IDataObject; 318 | const fields = (options.fieldsList as IDataObject[]) || []; 319 | const filter = this.getNodeParameter('filterRequest', i) as IOdooFilterOperations; 320 | if (returnAll) { 321 | responseData = await odooGetAll.call( 322 | this, 323 | db, 324 | userID, 325 | password, 326 | resource, 327 | operation, 328 | url, 329 | filter, 330 | fields, 331 | ); 332 | } else { 333 | const offset = this.getNodeParameter('offset', i) as number; 334 | const limit = this.getNodeParameter('limit', i) as number; 335 | responseData = await odooGetAll.call( 336 | this, 337 | db, 338 | userID, 339 | password, 340 | resource, 341 | operation, 342 | url, 343 | filter, 344 | fields, 345 | offset, 346 | limit, 347 | ); 348 | } 349 | } 350 | 351 | if (operation === 'update') { 352 | const id = this.getNodeParameter('id', i) as string; 353 | const fields = this.getNodeParameter('fieldsToCreateOrUpdate', i) as IDataObject; 354 | responseData = await odooUpdate.call( 355 | this, 356 | db, 357 | userID, 358 | password, 359 | resource, 360 | operation, 361 | url, 362 | id, 363 | processNameValueFields(fields), 364 | ); 365 | } 366 | 367 | if (operation === 'workflow') { 368 | const id = this.getNodeParameter('id', i) as string; 369 | const customOperation = this.getNodeParameter('customOperation', i) as string; 370 | responseData = await odooWorkflow.call( 371 | this, 372 | db, 373 | userID, 374 | password, 375 | resource, 376 | customOperation, 377 | url, 378 | id, 379 | ); 380 | } 381 | 382 | if (Array.isArray(responseData)) { 383 | returnData.push.apply(returnData, responseData); 384 | } else if (responseData !== undefined) { 385 | returnData.push(responseData); 386 | } 387 | } catch (error) { 388 | if (this.continueOnFail()) { 389 | returnData.push({ error: (error as JsonObject).message }); 390 | continue; 391 | } 392 | throw error; 393 | } 394 | } 395 | 396 | return [this.helpers.returnJsonArray(returnData)]; 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /nodes/Odoo/GenericFunctions.ts: -------------------------------------------------------------------------------- 1 | import { OptionsWithUri } from 'request'; 2 | 3 | import { 4 | IExecuteFunctions, 5 | IExecuteSingleFunctions, 6 | IHookFunctions, 7 | ILoadOptionsFunctions, 8 | } from 'n8n-core'; 9 | 10 | import { IDataObject, JsonObject, NodeApiError } from 'n8n-workflow'; 11 | 12 | const serviceJSONRPC = 'object'; 13 | const methodJSONRPC = 'execute'; 14 | 15 | export const mapOperationToJSONRPC = { 16 | create: 'create', 17 | get: 'read', 18 | getAll: 'search_read', 19 | update: 'write', 20 | delete: 'unlink', 21 | }; 22 | 23 | export const mapOdooResources: { [key: string]: string } = { 24 | contact: 'res.partner', 25 | opportunity: 'crm.lead', 26 | note: 'note.note', 27 | }; 28 | 29 | export const mapFilterOperationToJSONRPC = { 30 | equal: '=', 31 | notEqual: '!=', 32 | greaterThen: '>', 33 | lesserThen: '<', 34 | greaterOrEqual: '>=', 35 | lesserOrEqual: '<=', 36 | like: 'like', 37 | in: 'in', 38 | notIn: 'not in', 39 | childOf: 'child_of', 40 | }; 41 | 42 | type FilterOperation = 43 | | 'equal' 44 | | 'notEqual' 45 | | 'greaterThen' 46 | | 'lesserThen' 47 | | 'greaterOrEqual' 48 | | 'lesserOrEqual' 49 | | 'like' 50 | | 'in' 51 | | 'notIn' 52 | | 'childOf'; 53 | 54 | export interface IOdooFilterOperations { 55 | filter: Array<{ 56 | fieldName: string; 57 | operator: string; 58 | value: string | number; 59 | }>; 60 | } 61 | 62 | export interface IOdooNameValueFields { 63 | fields: Array<{ 64 | fieldName: string; 65 | fieldValue: string; 66 | }>; 67 | } 68 | 69 | export interface IOdooResponseFields { 70 | fields: Array<{ 71 | field: string; 72 | fromList?: boolean; 73 | }>; 74 | } 75 | 76 | type OdooCRUD = 'create' | 'update' | 'delete' | 'get' | 'getAll'; 77 | 78 | export function odooGetDBName(databaseName: string | undefined, url: string) { 79 | if (databaseName) return databaseName; 80 | const odooURL = new URL(url); 81 | const hostname = odooURL.hostname; 82 | if (!hostname) return ''; 83 | return odooURL.hostname.split('.')[0]; 84 | } 85 | 86 | function processFilters(value: IOdooFilterOperations) { 87 | return value.filter?.map((item) => { 88 | const operator = item.operator as FilterOperation; 89 | item.operator = mapFilterOperationToJSONRPC[operator]; 90 | return Object.values(item); 91 | }); 92 | } 93 | 94 | export function processNameValueFields(value: IDataObject) { 95 | const data = value as unknown as IOdooNameValueFields; 96 | return data?.fields?.reduce((acc, record) => { 97 | return Object.assign(acc, { [record.fieldName]: record.fieldValue }); 98 | }, {}); 99 | } 100 | 101 | // function processResponseFields(value: IDataObject) { 102 | // const data = value as unknown as IOdooResponseFields; 103 | // return data?.fields?.map((entry) => entry.field); 104 | // } 105 | 106 | export async function odooJSONRPCRequest( 107 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 108 | body: IDataObject, 109 | url: string, 110 | ): Promise { 111 | try { 112 | const options: OptionsWithUri = { 113 | headers: { 114 | 'User-Agent': 'n8n', 115 | Connection: 'keep-alive', 116 | Accept: '*/*', 117 | 'Content-Type': 'application/json', 118 | }, 119 | method: 'POST', 120 | body, 121 | uri: `${url}/jsonrpc`, 122 | json: true, 123 | }; 124 | 125 | const response = await this.helpers.request!(options); 126 | if (response.error) { 127 | throw new NodeApiError(this.getNode(), response.error.data, { 128 | message: response.error.data.message, 129 | }); 130 | } 131 | return response.result; 132 | } catch (error) { 133 | throw new NodeApiError(this.getNode(), error as JsonObject); 134 | } 135 | } 136 | 137 | export async function odooGetModelFields( 138 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 139 | db: string, 140 | userID: number, 141 | password: string, 142 | resource: string, 143 | url: string, 144 | ) { 145 | try { 146 | const body = { 147 | jsonrpc: '2.0', 148 | method: 'call', 149 | params: { 150 | service: serviceJSONRPC, 151 | method: methodJSONRPC, 152 | args: [ 153 | db, 154 | userID, 155 | password, 156 | mapOdooResources[resource] || resource, 157 | 'fields_get', 158 | [], 159 | ['string', 'type', 'help', 'required', 'name'], 160 | ], 161 | }, 162 | id: Math.floor(Math.random() * 100), 163 | }; 164 | 165 | const result = await odooJSONRPCRequest.call(this, body, url); 166 | return result; 167 | } catch (error) { 168 | throw new NodeApiError(this.getNode(), error as JsonObject); 169 | } 170 | } 171 | 172 | export async function odooIsAddonInstalled( 173 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 174 | ): Promise { 175 | try { 176 | const credentials = await this.getCredentials('odooApi'); 177 | const url = credentials?.url as string; 178 | const username = credentials?.username as string; 179 | const password = credentials?.password as string; 180 | const db = odooGetDBName(credentials?.db as string, url); 181 | const userID = await odooGetUserID.call(this, db, username, password, url); 182 | 183 | const body = { 184 | jsonrpc: '2.0', 185 | method: 'call', 186 | params: { 187 | service: serviceJSONRPC, 188 | method: methodJSONRPC, 189 | args: [db, userID, password, 'ir.model', 'search_read', [['model', '=', 'base']], []], 190 | }, 191 | id: Math.floor(Math.random() * 100), 192 | }; 193 | 194 | const result = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; 195 | if (result?.length === 1 && result[0].hasOwnProperty('methods')) { 196 | return true; 197 | } else { 198 | return false; 199 | } 200 | } catch (error) { 201 | return false; 202 | } 203 | } 204 | 205 | export async function odooGetActionMethods( 206 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 207 | db: string, 208 | userID: number, 209 | password: string, 210 | resource: string, 211 | url: string, 212 | ): Promise { 213 | try { 214 | const body = { 215 | jsonrpc: '2.0', 216 | method: 'call', 217 | params: { 218 | service: serviceJSONRPC, 219 | method: methodJSONRPC, 220 | args: [ 221 | db, 222 | userID, 223 | password, 224 | 'ir.model', 225 | 'search_read', 226 | [['model', '=', resource]], 227 | ['methods'], 228 | ], 229 | }, 230 | id: Math.floor(Math.random() * 100), 231 | }; 232 | 233 | const result = (await odooJSONRPCRequest.call(this, body, url)) as IDataObject[]; 234 | if (result?.length === 1 && result[0].hasOwnProperty('methods')) { 235 | const methods = JSON.parse(result[0].methods as string) as string[]; 236 | return methods; 237 | } else { 238 | return undefined; 239 | } 240 | } catch (error) { 241 | throw new NodeApiError(this.getNode(), error as JsonObject); 242 | } 243 | } 244 | 245 | export async function odooCreate( 246 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 247 | db: string, 248 | userID: number, 249 | password: string, 250 | resource: string, 251 | operation: OdooCRUD, 252 | url: string, 253 | newItem: IDataObject, 254 | ) { 255 | try { 256 | const body = { 257 | jsonrpc: '2.0', 258 | method: 'call', 259 | params: { 260 | service: serviceJSONRPC, 261 | method: methodJSONRPC, 262 | args: [ 263 | db, 264 | userID, 265 | password, 266 | mapOdooResources[resource] || resource, 267 | mapOperationToJSONRPC[operation], 268 | newItem || {}, 269 | ], 270 | }, 271 | id: Math.floor(Math.random() * 100), 272 | }; 273 | 274 | const result = await odooJSONRPCRequest.call(this, body, url); 275 | return { id: result }; 276 | } catch (error) { 277 | throw new NodeApiError(this.getNode(), error as JsonObject); 278 | } 279 | } 280 | 281 | export async function odooGet( 282 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 283 | db: string, 284 | userID: number, 285 | password: string, 286 | resource: string, 287 | operation: OdooCRUD, 288 | url: string, 289 | itemsID: string, 290 | fieldsToReturn?: IDataObject[], 291 | ) { 292 | try { 293 | if (!/^\d+$/.test(itemsID) || !parseInt(itemsID, 10)) { 294 | throw new NodeApiError(this.getNode(), { 295 | status: 'Error', 296 | message: `Please specify a valid ID: ${itemsID}`, 297 | }); 298 | } 299 | const body = { 300 | jsonrpc: '2.0', 301 | method: 'call', 302 | params: { 303 | service: serviceJSONRPC, 304 | method: methodJSONRPC, 305 | args: [ 306 | db, 307 | userID, 308 | password, 309 | mapOdooResources[resource] || resource, 310 | mapOperationToJSONRPC[operation], 311 | [+itemsID] || [], 312 | fieldsToReturn || [], 313 | ], 314 | }, 315 | id: Math.floor(Math.random() * 100), 316 | }; 317 | 318 | const result = await odooJSONRPCRequest.call(this, body, url); 319 | return result; 320 | } catch (error) { 321 | throw new NodeApiError(this.getNode(), error as JsonObject); 322 | } 323 | } 324 | 325 | export async function odooGetAll( 326 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 327 | db: string, 328 | userID: number, 329 | password: string, 330 | resource: string, 331 | operation: OdooCRUD, 332 | url: string, 333 | filters?: IOdooFilterOperations, 334 | fieldsToReturn?: IDataObject[], 335 | offset = 0, 336 | limit = 0, 337 | ) { 338 | try { 339 | const body = { 340 | jsonrpc: '2.0', 341 | method: 'call', 342 | params: { 343 | service: serviceJSONRPC, 344 | method: methodJSONRPC, 345 | args: [ 346 | db, 347 | userID, 348 | password, 349 | mapOdooResources[resource] || resource, 350 | mapOperationToJSONRPC[operation], 351 | (filters && processFilters(filters)) || [], 352 | fieldsToReturn || [], 353 | offset, 354 | limit, 355 | ], 356 | }, 357 | id: Math.floor(Math.random() * 100), 358 | }; 359 | 360 | const result = await odooJSONRPCRequest.call(this, body, url); 361 | return result; 362 | } catch (error) { 363 | throw new NodeApiError(this.getNode(), error as JsonObject); 364 | } 365 | } 366 | 367 | export async function odooUpdate( 368 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 369 | db: string, 370 | userID: number, 371 | password: string, 372 | resource: string, 373 | operation: OdooCRUD, 374 | url: string, 375 | itemsID: string, 376 | fieldsToUpdate: IDataObject, 377 | ) { 378 | try { 379 | if (!Object.keys(fieldsToUpdate).length) { 380 | throw new NodeApiError(this.getNode(), { 381 | status: 'Error', 382 | message: `Please specify at least one field to update`, 383 | }); 384 | } 385 | if (!/^\d+$/.test(itemsID) || !parseInt(itemsID, 10)) { 386 | throw new NodeApiError(this.getNode(), { 387 | status: 'Error', 388 | message: `Please specify a valid ID: ${itemsID}`, 389 | }); 390 | } 391 | const body = { 392 | jsonrpc: '2.0', 393 | method: 'call', 394 | params: { 395 | service: serviceJSONRPC, 396 | method: methodJSONRPC, 397 | args: [ 398 | db, 399 | userID, 400 | password, 401 | mapOdooResources[resource] || resource, 402 | mapOperationToJSONRPC[operation], 403 | [+itemsID] || [], 404 | fieldsToUpdate, 405 | ], 406 | }, 407 | id: Math.floor(Math.random() * 100), 408 | }; 409 | 410 | await odooJSONRPCRequest.call(this, body, url); 411 | return { id: itemsID }; 412 | } catch (error) { 413 | throw new NodeApiError(this.getNode(), error as JsonObject); 414 | } 415 | } 416 | 417 | export async function odooWorkflow( 418 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 419 | db: string, 420 | userID: number, 421 | password: string, 422 | resource: string, 423 | customOperation: string, 424 | url: string, 425 | itemsID: string, 426 | ) { 427 | try { 428 | if (!/^\d+$/.test(itemsID) || !parseInt(itemsID, 10)) { 429 | throw new NodeApiError(this.getNode(), { 430 | status: 'Error', 431 | message: `Please specify a valid ID: ${itemsID}`, 432 | }); 433 | } 434 | const body = { 435 | jsonrpc: '2.0', 436 | method: 'call', 437 | params: { 438 | service: 'object', 439 | method: 'execute', 440 | args: [db, userID, password, resource, customOperation, [+itemsID] || []], 441 | }, 442 | id: Math.floor(Math.random() * 100), 443 | }; 444 | 445 | const result = await odooJSONRPCRequest.call(this, body, url); 446 | return result; 447 | } catch (error) { 448 | throw new NodeApiError(this.getNode(), error as JsonObject); 449 | } 450 | } 451 | 452 | export async function odooDelete( 453 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 454 | db: string, 455 | userID: number, 456 | password: string, 457 | resource: string, 458 | operation: OdooCRUD, 459 | url: string, 460 | itemsID: string, 461 | ) { 462 | if (!/^\d+$/.test(itemsID) || !parseInt(itemsID, 10)) { 463 | throw new NodeApiError(this.getNode(), { 464 | status: 'Error', 465 | message: `Please specify a valid ID: ${itemsID}`, 466 | }); 467 | } 468 | try { 469 | const body = { 470 | jsonrpc: '2.0', 471 | method: 'call', 472 | params: { 473 | service: serviceJSONRPC, 474 | method: methodJSONRPC, 475 | args: [ 476 | db, 477 | userID, 478 | password, 479 | mapOdooResources[resource] || resource, 480 | mapOperationToJSONRPC[operation], 481 | [+itemsID] || [], 482 | ], 483 | }, 484 | id: Math.floor(Math.random() * 100), 485 | }; 486 | 487 | await odooJSONRPCRequest.call(this, body, url); 488 | return { success: true }; 489 | } catch (error) { 490 | throw new NodeApiError(this.getNode(), error as JsonObject); 491 | } 492 | } 493 | 494 | export async function odooGetUserID( 495 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 496 | db: string, 497 | username: string, 498 | password: string, 499 | url: string, 500 | ): Promise { 501 | try { 502 | const body = { 503 | jsonrpc: '2.0', 504 | method: 'call', 505 | params: { 506 | service: 'common', 507 | method: 'login', 508 | args: [db, username, password], 509 | }, 510 | id: Math.floor(Math.random() * 100), 511 | }; 512 | const loginResult = await odooJSONRPCRequest.call(this, body, url); 513 | return loginResult as unknown as number; 514 | } catch (error) { 515 | throw new NodeApiError(this.getNode(), error as JsonObject); 516 | } 517 | } 518 | 519 | export async function odooGetServerVersion( 520 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 521 | url: string, 522 | ) { 523 | try { 524 | const body = { 525 | jsonrpc: '2.0', 526 | method: 'call', 527 | params: { 528 | service: 'common', 529 | method: 'version', 530 | args: [], 531 | }, 532 | id: Math.floor(Math.random() * 100), 533 | }; 534 | const result = await odooJSONRPCRequest.call(this, body, url); 535 | return result; 536 | } catch (error) { 537 | throw new NodeApiError(this.getNode(), error as JsonObject); 538 | } 539 | } 540 | --------------------------------------------------------------------------------