npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@fernandoslim/odoo-jsonrpc

v2.0.0

Published

A lightweight Odoo JSON-RPC client with zero dependencies.

Downloads

525

Readme

Odoo JSON-RPC

A lightweight Odoo JSON-RPC client with zero dependencies.

Based on OdooAwait which uses XML-RPC. Special thanks to @vettloffah.

Performance

JSON-RPC significantly outperforms XML-RPC in our synthetic benchmark tests.

Synthetic Benchmark with HonoJS

hey -n 2000 -c 80 -m GET -H "Content-Type: application/json" -H "Authorization: Bearer honoiscool" http://localhost:3000/v1/contacts/3

JSON-RPC

Total: 3.2409 secs
Slowest: 0.4474 secs
Fastest: 0.0852 secs
Average: 0.1220 secs
Requests/sec: 617.1133

XML-RPC

Total: 5.6660 secs
Slowest: 0.7938 secs
Fastest: 0.0978 secs
Average: 0.2135 secs
Requests/sec: 352.9848

Based on these results:

  • JSON-RPC processes approximately 75% more requests per second than XML-RPC.
  • JSON-RPC's average response time is about 43% faster than XML-RPC.

Node version

Node 18+ Designed to work with Cloudflare Workers

Installation

npm install odoo-jsonrpc

Helpers

Try

Introduced the Try helper, which encapsulates a try/catch block in a smart way. This allows you to make requests and handle responses and errors more reliably, similar to Go.

// Getting a contact by id
export const getContactById = async (contact_id: number) => {
  const [contacts, error] = await Try(() => odoo.read('res.partner', contact_id, ['name', 'email', 'mobile']));
  if (error) {
    throw error;
  }
  if (contacts.length === 0) {
    throw new Error('Contact Not Found.');
  }
  const [contact] = contacts;
  return contact;
};
// Create and confirm a Sales Order
export const createSalesOrder = async (salesorder_data: SalesOrder) => {
  // Creating Sales Order
  const [salesorder_id, creating_salesorder_error] = await Try(() => odoo.create('sale.order', salesorder_data));
  if (creating_salesorder_error) {
    throw creating_salesorder_error;
  }
  // Confirming Sales Order
  // If the Sales Order is confirmed, it will return a boolean. Since this value is not used, the underscore (_) is used as a placeholder.
  const [_, confirming_salesorder_error] = await Try(() => odoo.action('sale.order', 'action_confirm', [salesorder_id]));
  if (confirming_salesorder_error) {
    throw confirming_salesorder_error;
  }
  return salesorder_id;
};

Cloudflare

import { Hono } from 'hono';

type Bindings = {
  ODOO_BASE_URL: string;
  ODOO_PORT: number;
  ODOO_DB: string;
  ODOO_USERNAME: string;
  ODOO_PASSWORD: string;
  ODOO_API_KEY: string;
};
const app = new Hono<{ Bindings: Bindings }>();
const odoo = new OdooJSONRpc();

app.use('/odoo/*', async (c, next) => {
  if (!odoo.is_connected) {
    await odoo.connect({
      baseUrl: c.env.ODOO_BASE_URL,
      port: c.env.ODOO_PORT,
      db: c.env.ODOO_DB,
      username: c.env.ODOO_USERNAME,
      apiKey: c.env.ODOO_API_KEY,
    });
  }
  return next();
});

// Get res.partner by id
app.get('/odoo/contacts/:id', async (c) => {
  const { id } = c.req.param();
  const [contacts, error] = await Try(() => odoo.read<{ id: number; name: string; email: string }>('res.partner', parseInt(id), ['name', 'email']));
  if (error) {
    return c.text(error.message, 422);
  }
  if (contacts.length === 0) {
    return c.text('not found', 404);
  }
  const [contact] = contacts;
  return c.json(contact, 200);
});

Node

import OdooJSONRpc from '@fernandoslim/odoo-jsonrpc';

// Authenticating with username and password
const odoo = new OdooJSONRpc({
  baseUrl: process.env.ODOO_BASE_URL!,
  port: Number(process.env.ODOO_PORT!),
  db: process.env.ODOO_DB!,
  username: process.env.ODOO_USERNAME!,
  password: process.env.ODOO_PASSWORD!,
});

await odoo.connect();
// Working with existing sessionId
const odoo = new OdooJSONRpc({
  baseUrl: process.env.ODOO_BASE_URL!,
  port: Number(process.env.ODOO_PORT!),
  db: process.env.ODOO_DB!,
  sessionId: "12eb065d6b17d27723a72f5dcb0d85071ae346e2"
});
// Authenticating with api key
const odoo = new OdooJSONRpc({
  baseUrl: process.env.ODOO_BASE_URL!,
  port: Number(process.env.ODOO_PORT!),
  db: process.env.ODOO_DB!,
  username: process.env.ODOO_USERNAME!,
  apiKey: 'c721a30555935cbabe8851df3f3eb9e60e850711'
});
// Authenticate and connect to the Odoo server
// This method returns different types of responses depending on the authentication method:
// - When using credentials or a session ID, it returns a full OdooAuthenticateWithCredentialsResponse
// - When using an API key, it returns a simpler OdooAuthenticateWithApiKeyResponse
// The full response (OdooAuthenticateWithCredentialsResponse) contains additional user and system information
const authResponse = await odoo.connect();
// Type guard to determine if the authentication response is a full credentials response.
// This function distinguishes between the two possible authentication response types:
// - OdooAuthenticateWithCredentialsResponse (full response with user details)
// - OdooAuthenticateWithApiKeyResponse (simple response with just the user ID)
const authResponse = await odoo.connect();
if (isCredentialsResponse(authResponse)) {
  console.log('Authenticated user:', authResponse.username);
} else {
  console.log('Authenticated with API key, user ID:', authResponse.uid);
}
const partnerId = await odoo.create('res.partner', {
  name: 'Kool Keith',
  email: '[email protected]',
});
console.log(`Partner created with ID ${partnerId}`);

// If connecting to a dev instance of odoo.sh, your config will looking something like:
const odoo = new OdooJSONRpc({
  baseUrl: 'https://some-database-name-5-29043948.dev.odoo.com/',
  port: 443,
  db: 'some-database-name-5-29043948',
  username: 'myusername',
  password: 'somepassword',
});

Methods

odoo.connect()

Must be called before other methods.

odoo.call_kw(model,method,args,kwargs)

This method is wrapped inside the below methods. If below methods don't do what you need, you can use this method. Docs: Odoo External API

odoo.action(model, action, recordId)

Execute a server action on a record or a set of records. Oddly, the Odoo API returns false if it was successful.

await odoo.action('account.move', 'action_post', [126996, 126995]);

CRUD

odoo.create(model, params, externalId)

Returns the ID of the created record. The externalId parameter is special. If supplied, will create a linked record in the ir.model.data model. See the "working with external identifiers" section below for more information.

const partnerId = await odoo.create('res.partner', { name: 'Kool Keith' });

odoo.read(model, recordId, fields)

Takes an array of record ID's and fetches the record data. Returns an array. Optionally, you can specify which fields to return. This is usually a good idea, since there tends to be a lot of fields on the base models (like over 100). The record ID is always returned regardless of fields specified.

const records = await odoo.read('res.partner', [54, 1568], ['name', 'email']);
console.log(records);
// [ { id: 127362, name: 'Kool Keith', email: '[email protected] }, { id: 127883, name: 'Jack Dorsey', email: '[email protected]' } ];

odoo.update(model, recordId, params)

Returns true if successful

const updated = await odoo.update('res.partner', 54, {
  street: '334 Living Astro Blvd.',
});
console.log(updated); // true

odoo.delete(model, recordId)

Returns true if successful.

const deleted = await odoo.delete('res.partner', 54);

many2many and one2many fields

Odoo handles the related field lists in a special way. You can choose to:

  1. add an existing record to the list using the record ID
  2. update an existing record in the record set using ID and new values
  3. create a new record on the fly and add it to the list using values
  4. replace all records with other record(s) without deleting the replaced ones from database - using a list of IDs
  5. delete one or multiple records from the database

In order to use any of these actions on a field, supply an object as the field value with the following parameters:

  • action (required) - one of the strings from above
  • id (required for actions that use id(s) ) - can usually be an array, or a single number
  • value (required for actions that update or create new related records) - can usually be an single value object, or an array of value objects if creating mutliple records

Examples

// Create new realted records on the fly
await odoo.update('res.partner', 278, {
  category_id: {
    action: 'create',
    value: [{ name: 'a new category' }, { name: 'another new category' }],
  },
});

// Update a related record in the set
await odoo.update('res.partner', 278, {
  category_id: {
    action: 'update',
    id: 3,
    value: { name: 'Updated category' },
  },
});

// Add existing records to the set
await odoo.update('res.partner', 278, {
  category_id: {
    action: 'add',
    id: 5, // or an array of numbers
  },
});

// Remove from the set but don't delete from database
await odoo.update('res.partner', 278, {
  category_id: {
    action: 'remove',
    id: 5, // or an array of numbers
  },
});

// Remove record and delete from database
await odoo.update('res.partner', 278, {
  category_id: {
    action: 'delete',
    id: 5, // or an array of numbers
  },
});

// Clear all records from set, but don't delete
await odoo.update('res.partner', 278, {
  category_id: {
    action: 'clear',
  },
});

// Replace records in set with other existing records
await odoo.update('res.partner', 278, {
  category_id: {
    action: 'replace',
    id: [3, 12, 6], // or a single number
  },
});

// You can also just do a regular update with an array of IDs, which will accomplish same as above
await odoo.update('res.partner', 278, {
  category_id: [3, 12, 16],
});

Other Odoo API Methods

odoo.search(model, domain)

Searches and returns record ID's for all records that match the model and domain.

const recordIds = await odoo.search(`res.partner`, {
  country_id: 'United States',
});
console.log(recordIds); // [14,26,33, ... ]

// Return all records of a certain model (omit domain)
const records = await odoo.searchRead(`res.partner`);

odoo.searchRead(model, domain, fields, opts)

Searches for matching records and returns record data. Provide an array of field names if you only want certain fields returned.

const records = await odoo.searchRead(`res.partner`, [['country_id', '=', 'United States']], ['name', 'city'], {
  limit: 5,
  offset: 10,
  order: 'name, desc',
  context: { lang: 'en_US' },
});
console.log(records); // [ { id: 5, name: 'Kool Keith', city: 'Los Angeles' }, ... ]

// Empty domain or other args can be used
const records = await odoo.searchRead(`res.partner`, [], ['name', 'city'], {
  limit: 10,
  offset: 20,
});

Complex domain filters

A domain filter array can be supplied if any of the alternate domain filters are needed, such as <, >, like, =like, ilike, in etc. For a complete list check out the API Docs. You can also use the logical operators OR "|", AND "&", NOT "!". Works in both the search() and searchRead() functions.

// Single domain filter array
const recordIds = await odoo.search('res.partner', ['name', '=like', 'john%']);

// Or a multiple domain filter array (array of arrays)
const recordIds = await odoo.search('res.partner', [
  ['name', '=like', 'john%'],
  ['sale_order_count', '>', 1],
]);

// Logical operator OR
// email is "[email protected]" OR name includes "charlie"
const records = await odoo.searchRead('res.partner', ['|', ['email', '=', '[email protected]'], ['name', 'ilike', 'charlie']]);

odoo.getFields(model, attributes)

Returns detailed list of fields for a model, filtered by attributes. e.g., if you only want to know if fields are required you could call:

const fields = await odoo.getFields('res.partner', ['required']);
console.log(fields);

Working With External Identifiers

External ID's can be important when using the native Odoo import feature with CSV files to sync data between systems, or updating records using your own unique identifiers instead of the Odoo database ID.

External ID's are created automatically when exporting or importing data using the Odoo user interface, but when working with the API this must be done intentionally.

External IDs are managed separately in the ir.model.data model in the database - so these methods make working with them easier.

Module names with external ID's

External ID's require a module name along with the ID. If you don't supply a module name when creating an external ID with this library, the default module name 'api' will be used. What that means is that 'some-unique-identifier' will live in the database as '__api__.some-unique-identifier'. You do not need to supply the module name when searching using externalId.

create(model, params, externalId, moduleName)

If creating a record, simply supply the external ID as the third parameter, and a module name as an optional 4th parameter. This example creates a record and an external ID in one method. (although it makes two separate create calls to the database under the hood).

const record = await odoo.create('product.product', { name: 'new product' }, 'some-unique-identifier');

createExternalId(model, recordId, externalId)

For records that are already created without an external ID, you can link an external ID to it.

await odoo.createExternalId('product.product', 76, 'some-unique-identifier');

readByExternalId(externalId, fields);

Find a record by the external ID, and return whatever fields you want. Leave the fields parameter empty to return all fields.

const record = await odoo.readByExternalId('some-unique-identifier', ['name', 'email']);

updateByExternalId(externalId, params)

const updated = await odoo.updateByExternalId('some-unique-identifier', {
  name: 'space shoe',
  price: 65479.99,
});

License

ISC

Copyright 2024 Fernando Delgado

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.