├── .gitignore ├── gulpfile.js ├── .jshintrc ├── examples └── basic.js ├── package.json ├── test └── test.js ├── README.md └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | mocha = require('gulp-mocha'); 3 | 4 | gulp.task('test', function () { 5 | return gulp.src(['./test/**/*.js'], { read: false }) 6 | .pipe(mocha({ reporter: 'spec' })); 7 | }); 8 | 9 | gulp.task('default', function () { 10 | gulp.watch(['./lib/**/*', './test/**/*'], ['test']); 11 | }); 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": false, 3 | "eqeqeq": false, 4 | "indent": false, 5 | "newcap": false, 6 | "plusplus": false, 7 | "quotmark": false, 8 | "unused": false, 9 | "strict": false, 10 | "trailing": false, 11 | "maxstatements": false, 12 | "maxlen": false, 13 | 14 | "eqnull": true, 15 | "smarttabs": true, 16 | 17 | "browser": true, 18 | "node": true 19 | } 20 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | var Odoo = require('../lib/index'); 2 | 3 | var odoo = new Odoo({ 4 | host: 'localhost', 5 | port: 4569, 6 | database: '4yopping', 7 | username: 'admin', 8 | password: '4yopping' 9 | }); 10 | 11 | // Connect to Odoo 12 | odoo.connect(function (err) { 13 | if (err) { return console.log(err); } 14 | 15 | // Get a partner 16 | odoo.get('res.partner', 4, function (err, partner) { 17 | if (err) { return console.log(err); } 18 | 19 | console.log('Partner', partner); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odoo", 3 | "version": "0.4.0", 4 | "description": "Node.js client library for Odoo using JSON-RPC", 5 | "main": "./lib/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha test" 11 | }, 12 | "engines": { 13 | "node": ">=0.11.16" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/saidimu/odoo" 18 | }, 19 | "keywords": [ 20 | "odoo", 21 | "openepr", 22 | "jsonrpc" 23 | ], 24 | "author": "Saidimu Apale ", 25 | "bugs": { 26 | "url": "https://github.com/saidimu/odoo/issues" 27 | }, 28 | "homepage": "https://github.com/saidimu/odoo", 29 | "dependencies": { 30 | "jayson": "^1.2.1", 31 | "lodash": "^3.10.1" 32 | }, 33 | "devDependencies": { 34 | "gulp": "^3.8.11", 35 | "gulp-mocha": "^2.0.0", 36 | "sinon": "^1.12.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | sinon = require('sinon'), 3 | Odoo = require('../lib/index'); 4 | 5 | var config = { 6 | host: 'odoo4yopping.vagrantshare.com', 7 | port: 80, 8 | database: '4yopping', 9 | username: 'admin', 10 | password: '4yopping' 11 | }; 12 | 13 | var odoo = new Odoo(config); 14 | 15 | describe('Odoo', function () { 16 | this.timeout(3000); 17 | 18 | it('Odoo should be a function', function () { 19 | assert.equal(typeof Odoo, 'function'); 20 | }); 21 | 22 | it('odoo should be an instance of Odoo', function () { 23 | assert(odoo instanceof Odoo); 24 | }); 25 | 26 | it('odoo should have this properties', function () { 27 | assert.notEqual(odoo.host, undefined); 28 | assert.equal(odoo.host, config.host); 29 | assert.notEqual(odoo.port, undefined); 30 | assert.equal(odoo.port, config.port); 31 | assert.notEqual(odoo.database, undefined); 32 | assert.equal(odoo.database, config.database); 33 | assert.notEqual(odoo.username, undefined); 34 | assert.equal(odoo.username, config.username); 35 | assert.notEqual(odoo.password, undefined); 36 | assert.equal(odoo.password, config.password); 37 | }); 38 | 39 | it('odoo should have this public functions', function () { 40 | assert.equal(typeof odoo.connect, 'function'); 41 | assert.equal(typeof odoo.create, 'function'); 42 | assert.equal(typeof odoo.get, 'function'); 43 | assert.equal(typeof odoo.update, 'function'); 44 | assert.equal(typeof odoo.delete, 'function'); 45 | assert.equal(typeof odoo.search, 'function'); 46 | }); 47 | 48 | it('odoo should have this private functions', function () { 49 | assert.equal(typeof odoo._request, 'function'); 50 | }); 51 | 52 | describe('Creating client', function () { 53 | 54 | it('client should not be able to connect to odoo server', function (done) { 55 | var client = new Odoo({ 56 | host: config.host, 57 | database: 'DatabaseNotFound', 58 | username: config.username, 59 | password: config.password 60 | }), 61 | callback = sinon.spy(); 62 | 63 | client.connect(callback); 64 | 65 | setTimeout(function () { 66 | assert(callback.called); 67 | assert.equal(typeof callback.args[0][0], 'object'); 68 | assert.equal(callback.args[0][1], null); 69 | 70 | done(); 71 | }, 2000); 72 | }); 73 | 74 | it('client should be able to connect to odoo server', function (done) { 75 | var callback = sinon.spy(); 76 | 77 | odoo.connect(callback); 78 | 79 | setTimeout(function () { 80 | assert(callback.calledWith(null)); 81 | assert.equal(typeof callback.args[0][1], 'object'); 82 | assert(odoo.uid); 83 | assert(odoo.sid); 84 | assert(odoo.session_id); 85 | assert(odoo.context); 86 | 87 | done(); 88 | }, 2000); 89 | }); 90 | 91 | }); 92 | 93 | describe('Records', function () { 94 | 95 | var created; 96 | 97 | it('client should create a record', function (done) { 98 | var callback = sinon.spy(); 99 | odoo.create('hr.employee', { 100 | name: 'John Doe', 101 | work_email: 'john@doe.com' 102 | }, callback); 103 | 104 | setTimeout(function () { 105 | assert(callback.calledWith(null)); 106 | assert.equal(typeof callback.args[0][1], 'number'); 107 | 108 | created = callback.args[0][1]; 109 | 110 | done(); 111 | }, 2000); 112 | 113 | }); 114 | 115 | it('client should get a record', function (done) { 116 | var callback = sinon.spy(); 117 | odoo.get('hr.employee', created, callback); 118 | 119 | setTimeout(function () { 120 | assert(callback.calledWith(null)); 121 | assert.equal(typeof callback.args[0][1], 'object'); 122 | assert.equal(callback.args[0][1].display_name, 'John Doe'); 123 | assert.equal(callback.args[0][1].work_email, 'john@doe.com'); 124 | 125 | done(); 126 | }, 2000); 127 | 128 | }); 129 | 130 | it('client should update a record', function (done) { 131 | var callback = sinon.spy(); 132 | odoo.update('hr.employee', created, { 133 | name: 'Jane Doe', 134 | work_email: 'jane@doe.com' 135 | }, callback); 136 | 137 | setTimeout(function () { 138 | assert(callback.calledWith(null)); 139 | assert(callback.args[0][1]); 140 | 141 | done(); 142 | }, 2000); 143 | }); 144 | 145 | it('client should delete a record', function (done) { 146 | var callback = sinon.spy(); 147 | odoo.delete('hr.employee', created, callback); 148 | 149 | setTimeout(function () { 150 | assert(callback.calledWith(null)); 151 | assert(callback.args[0][1]); 152 | 153 | done(); 154 | }, 2000); 155 | }); 156 | 157 | it('client should search records', function (done) { 158 | var callback = sinon.spy(); 159 | odoo.search('hr.employee', [['login', '=', 'admin']], callback); 160 | 161 | setTimeout(function () { 162 | assert(callback.calledWith(null)); 163 | assert.equal(typeof callback.args[0][1], 'array'); 164 | 165 | done(); 166 | }, 2000); 167 | }); 168 | 169 | }); 170 | 171 | }); 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Odoo 2 | 3 | Node.js client library for Odoo 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ npm install Odoo 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | var Odoo = require('Odoo'); 15 | 16 | var odoo = new Odoo({ 17 | host: 'localhost', 18 | port: 8069, 19 | database: 'demo', 20 | username: 'admin', 21 | password: 'admin' 22 | }); 23 | 24 | // Connect to Odoo 25 | odoo.connect(function (err) { 26 | if (err) { return console.log(err); } 27 | }); 28 | 29 | // Get a partner 30 | // https://www.odoo.com/documentation/8.0/api_integration.html#read-records 31 | // https://www.odoo.com/documentation/8.0/reference/orm.html#openerp.models.Model.read 32 | var params = { 33 | ids: [1,2,3,4,5], 34 | fields: [ 'name' ], 35 | }; //params 36 | odoo.get('res.partner', params, function (err, partners) { 37 | if (err) { return console.log(err); } 38 | 39 | console.log(partners); 40 | // [ 41 | // { id: 1, name: 'Demo Company' }, 42 | // { id: 3, name: 'Administrator' }, 43 | // { id: 4, name: 'Public user' }, 44 | // { id: 5, name: 'Demo User' } 45 | // ] 46 | }); //get 47 | 48 | 49 | // Search & Get products in one RPC call 50 | // https://www.odoo.com/documentation/8.0/api_integration.html#search-and-read 51 | // https://www.odoo.com/documentation/8.0/reference/orm.html#openerp.models.Model.search 52 | // https://www.odoo.com/documentation/8.0/reference/orm.html#openerp.models.Model.read 53 | var params = { 54 | ids: [1,2,3,4,5], 55 | domain: [ [ 'list_price', '>', '50' ], [ 'list_price', '<', '65' ] ], 56 | fields: [ 'name', 'list_price', 'items' ], 57 | order: 'list_price', 58 | limit: 5, 59 | offset: 0, 60 | }; //params 61 | odoo.search_read('product.product', params, function (err, products) { 62 | if (err) { return console.log(err); } 63 | 64 | console.log(products); 65 | // [ 66 | // { list_price: 60, id: 52, name: 'Router R430' }, 67 | // { list_price: 62, id: 39, name: 'Headset standard' } 68 | // ] 69 | 70 | }); //search_read 71 | 72 | 73 | // Browse products by ID 74 | // Not a direct implementation of Odoo RPC 'browse' but rather a workaround based on 'search_read' 75 | // https://www.odoo.com/documentation/8.0/reference/orm.html#openerp.models.Model.browse 76 | var params = { 77 | fields: [ 'name', 'list_price'], 78 | limit: 5, 79 | offset: 0, 80 | }; //params 81 | odoo.browse_by_id('product.product', params, function (err, products) { 82 | if (err) { return console.log(err); } 83 | 84 | console.log(products); 85 | // [ 86 | // { list_price: 4.49, id: 1180, name: 'Fruit Cup' }, 87 | // { list_price: 0, id: 1139, name: 'Orange Crush' }, 88 | // { list_price: 1.59, id: 1062, name: 'Blueberry muffin' }, 89 | // { list_price: 1.35, id: 1381, name: 'Otis Harvest Bran' } 90 | // ] 91 | }); //browse_by_id 92 | 93 | 94 | // Generic RPC call 95 | // Note that, unlike the other methods, the context is not automatically included 96 | var endpoint = '/web/dataset/call_kw'; 97 | var model = 'sale.order'; 98 | var method = 'action_view_delivery'; 99 | 100 | var args = [ 101 | [sale_order_id], 102 | { 103 | tz: odoo.context.tz, 104 | uid: odoo.context.uid, 105 | }, 106 | ];//args 107 | 108 | var params = { 109 | model: model, 110 | method: method, 111 | args: args, 112 | kwargs: {}, 113 | };//params 114 | 115 | // View Delivery Order 116 | odoo.rpc_call(endpoint, params, function(err, result) { 117 | if(err) { 118 | console.log(err); 119 | return; 120 | }//if 121 | 122 | var delivery_order = result; 123 | 124 | console.log(delivery_order); 125 | });//odoo.rpc_call 126 | 127 | 128 | ``` 129 | 130 | ## Methods 131 | 132 | * odoo.connect(callback) 133 | * odoo.get(model, id, callback) 134 | * odoo.search(model, params, callback) 135 | * odoo.search_read(model, params, callback) 136 | * odoo.browse_by_id(model, params, callback) 137 | * odoo.create(model, params, callback) 138 | * odoo.update(model, id, params, callback) 139 | * odoo.delete(model, id, callback) 140 | * odoo.rpc_call(endpoint, params, callback); 141 | 142 | ##Node version 143 | Works better with NodeJS v11.16 and further 144 | 145 | ## Reference 146 | 147 | * [Odoo Technical Documentation](https://www.odoo.com/documentation/8.0) 148 | * [Odoo Web Service API](https://www.odoo.com/documentation/8.0/api_integration.html) 149 | 150 | ## License 151 | 152 | The MIT License (MIT) 153 | 154 | Copyright (c) 2015 Marco Godínez, 4yopping and all the related trademarks 155 | 156 | Permission is hereby granted, free of charge, to any person obtaining a copy 157 | of this software and associated documentation files (the "Software"), to deal 158 | in the Software without restriction, including without limitation the rights 159 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 160 | copies of the Software, and to permit persons to whom the Software is 161 | furnished to do so, subject to the following conditions: 162 | 163 | The above copyright notice and this permission notice shall be included in 164 | all copies or substantial portions of the Software. 165 | 166 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 167 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 168 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 169 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 170 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 171 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 172 | THE SOFTWARE. 173 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | 5 | var http = require('http'), 6 | jayson = require('jayson'), 7 | _ = require('lodash'); 8 | 9 | var Odoo = function (config) { 10 | config = config || {}; 11 | 12 | this.host = config.host; 13 | this.port = config.port || 80; 14 | this.database = config.database; 15 | this.username = config.username; 16 | this.password = config.password; 17 | }; 18 | 19 | // Connect 20 | Odoo.prototype.connect = function (cb) { 21 | var params = { 22 | db: this.database, 23 | login: this.username, 24 | password: this.password 25 | }; 26 | 27 | var json = JSON.stringify({ params: params }); 28 | 29 | var options = { 30 | host: this.host, 31 | port: this.port, 32 | path: '/web/session/authenticate', 33 | method: 'POST', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | 'Accept': 'application/json', 37 | 'Content-Length': json.length 38 | } 39 | }; 40 | 41 | var self = this; 42 | 43 | var req = http.request(options, function (res) { 44 | var response = ''; 45 | 46 | res.setEncoding('utf8'); 47 | 48 | res.on('data', function (chunk) { 49 | response += chunk; 50 | }); 51 | 52 | res.on('end', function () { 53 | response = JSON.parse(response); 54 | 55 | if (response.error) { 56 | return cb(response.error, null); 57 | } 58 | 59 | self.uid = response.result.uid; 60 | self.sid = res.headers['set-cookie'][0].split(';')[0]; 61 | self.session_id = response.result.session_id; 62 | self.context = response.result.user_context; 63 | 64 | return cb(null, response.result); 65 | }); 66 | }); 67 | 68 | req.write(json); 69 | }; 70 | 71 | // Search records 72 | Odoo.prototype.search = function (model, params, callback) { 73 | // assert(params.ids, "Must provide a list of IDs."); 74 | assert(params.domain, "Must provide a search domain."); 75 | 76 | this._request('/web/dataset/call_kw', { 77 | kwargs: { 78 | context: this.context 79 | }, 80 | model: model, 81 | method: 'search', 82 | args: [ 83 | params.domain, 84 | ], 85 | }, callback); 86 | }; 87 | 88 | // Search & Read records 89 | // https://www.odoo.com/documentation/8.0/api_integration.html#search-and-read 90 | // https://www.odoo.com/documentation/8.0/reference/orm.html#openerp.models.Model.search 91 | // https://www.odoo.com/documentation/8.0/reference/orm.html#openerp.models.Model.read 92 | Odoo.prototype.search_read = function (model, params, callback) { 93 | assert(params.domain, "'domain' parameter required. Must provide a search domain."); 94 | assert(params.limit, "'limit' parameter required. Must specify max. number of results to return."); 95 | 96 | this._request('/web/dataset/call_kw', { 97 | model: model, 98 | method: 'search_read', 99 | args: [], 100 | kwargs: { 101 | context: this.context, 102 | domain: params.domain, 103 | offset: params.offset, 104 | limit: params.limit, 105 | order: params.order, 106 | fields: params.fields, 107 | }, 108 | }, callback); 109 | }; 110 | 111 | // Get record 112 | // https://www.odoo.com/documentation/8.0/api_integration.html#read-records 113 | // https://www.odoo.com/documentation/8.0/reference/orm.html#openerp.models.Model.read 114 | Odoo.prototype.get = function (model, params, callback) { 115 | assert(params.ids, "Must provide a list of IDs."); 116 | 117 | this._request('/web/dataset/call_kw', { 118 | model: model, 119 | method: 'read', 120 | args: [ 121 | params.ids, 122 | ], 123 | kwargs: { 124 | fields: params.fields, 125 | }, 126 | }, callback); 127 | }; //get 128 | 129 | 130 | // Browse records by ID 131 | // Not a direct implementation of Odoo RPC 'browse' but rather a workaround based on 'search_read' 132 | // https://www.odoo.com/documentation/8.0/reference/orm.html#openerp.models.Model.browse 133 | Odoo.prototype.browse_by_id = function(model, params, callback) { 134 | params.domain = [['id', '>', '0' ]]; // assumes all records IDs are > 0 135 | this.search_read(model, params, callback); 136 | }; //browse 137 | 138 | 139 | // Create record 140 | Odoo.prototype.create = function (model, params, callback) { 141 | this._request('/web/dataset/call_kw', { 142 | kwargs: { 143 | context: this.context 144 | }, 145 | model: model, 146 | method: 'create', 147 | args: [params] 148 | }, callback); 149 | }; 150 | 151 | // Update record 152 | Odoo.prototype.update = function (model, id, params, callback) { 153 | if (id) { 154 | this._request('/web/dataset/call_kw', { 155 | kwargs: { 156 | context: this.context 157 | }, 158 | model: model, 159 | method: 'write', 160 | args: [[id], params] 161 | }, callback); 162 | } 163 | }; 164 | 165 | // Delete record 166 | Odoo.prototype.delete = function (model, id, callback) { 167 | this._request('/web/dataset/call_kw', { 168 | kwargs: { 169 | context: this.context 170 | }, 171 | model: model, 172 | method: 'unlink', 173 | args: [[id]] 174 | }, callback); 175 | }; 176 | 177 | 178 | // Generic RPC wrapper 179 | // DOES NOT AUTO-INCLUDE context 180 | Odoo.prototype.rpc_call = function (endpoint, params, callback) { 181 | assert(params.model); 182 | // assert(params.method); 183 | // assert(params.args); 184 | // assert(params.kwargs); 185 | // assert(params.kwargs.context); 186 | 187 | this._request(endpoint, params, callback); 188 | }; //generic 189 | 190 | 191 | // Private functions 192 | Odoo.prototype._request = function (path, params, callback) { 193 | params = params || {}; 194 | 195 | var options = { 196 | host: this.host, 197 | port: this.port, 198 | path: path || '/', 199 | headers: { 200 | 'Content-Type': 'application/json', 201 | 'Accept': 'application/json', 202 | 'Cookie': this.sid + ';' 203 | } 204 | }; 205 | 206 | var client = jayson.client.http(options); 207 | 208 | client.request('call', params, function (e, err, res) { 209 | if (e || err) { 210 | return callback(e || err, null); 211 | } 212 | 213 | return callback(null, res); 214 | }); 215 | }; 216 | 217 | module.exports = Odoo; 218 | --------------------------------------------------------------------------------