import xmlrpc from "xmlrpc";
export interface OdooConfig {
url: string;
db: string;
username: string;
password: string;
}
export interface SearchReadOptions {
fields?: string[];
limit?: number;
offset?: number;
order?: string;
}
export class OdooClient {
private config: OdooConfig;
private uid: number | null = null;
private commonClient: xmlrpc.Client;
private objectClient: xmlrpc.Client;
constructor(config: OdooConfig) {
this.config = config;
const urlObj = new URL(config.url);
const isSecure = urlObj.protocol === "https:";
const port = urlObj.port
? parseInt(urlObj.port)
: isSecure
? 443
: 80;
const clientOptions = {
host: urlObj.hostname,
port: port,
path: "/xmlrpc/2/common",
};
if (isSecure) {
this.commonClient = xmlrpc.createSecureClient({
...clientOptions,
path: "/xmlrpc/2/common",
});
this.objectClient = xmlrpc.createSecureClient({
...clientOptions,
path: "/xmlrpc/2/object",
});
} else {
this.commonClient = xmlrpc.createClient({
...clientOptions,
path: "/xmlrpc/2/common",
});
this.objectClient = xmlrpc.createClient({
...clientOptions,
path: "/xmlrpc/2/object",
});
}
}
private methodCall<T>(
client: xmlrpc.Client,
method: string,
params: any[]
): Promise<T> {
return new Promise((resolve, reject) => {
client.methodCall(method, params, (error, value) => {
if (error) {
reject(error);
} else {
resolve(value as T);
}
});
});
}
async authenticate(): Promise<number> {
if (this.uid) return this.uid;
const uid = await this.methodCall<number | false>(
this.commonClient,
"authenticate",
[this.config.db, this.config.username, this.config.password, {}]
);
if (!uid) {
throw new Error("Authentication failed");
}
this.uid = uid;
return uid;
}
async execute<T>(
model: string,
method: string,
args: any[],
kwargs: Record<string, any> = {}
): Promise<T> {
await this.authenticate();
return this.methodCall<T>(this.objectClient, "execute_kw", [
this.config.db,
this.uid,
this.config.password,
model,
method,
args,
kwargs,
]);
}
async search(
model: string,
domain: any[][],
options: { limit?: number; offset?: number; order?: string } = {}
): Promise<number[]> {
return this.execute<number[]>(model, "search", [domain], options);
}
async searchRead<T = Record<string, any>>(
model: string,
domain: any[][],
options: SearchReadOptions = {}
): Promise<T[]> {
const kwargs: Record<string, any> = {};
if (options.fields) kwargs.fields = options.fields;
if (options.limit) kwargs.limit = options.limit;
if (options.offset) kwargs.offset = options.offset;
if (options.order) kwargs.order = options.order;
return this.execute<T[]>(model, "search_read", [domain], kwargs);
}
async read<T = Record<string, any>>(
model: string,
ids: number[],
fields?: string[]
): Promise<T[]> {
const kwargs: Record<string, any> = {};
if (fields) kwargs.fields = fields;
return this.execute<T[]>(model, "read", [ids], kwargs);
}
async create(model: string, values: Record<string, any>): Promise<number> {
return this.execute<number>(model, "create", [values]);
}
async write(
model: string,
ids: number[],
values: Record<string, any>
): Promise<boolean> {
return this.execute<boolean>(model, "write", [ids, values]);
}
async unlink(model: string, ids: number[]): Promise<boolean> {
return this.execute<boolean>(model, "unlink", [ids]);
}
async searchCount(model: string, domain: any[][]): Promise<number> {
return this.execute<number>(model, "search_count", [domain]);
}
async fieldsGet(
model: string,
fields?: string[]
): Promise<Record<string, any>> {
const kwargs: Record<string, any> = {};
if (fields) kwargs.allfields = fields;
return this.execute<Record<string, any>>(model, "fields_get", [], kwargs);
}
}