Pular para o conteúdo principal

API REST Odoo: exemplos práticos e tutorial de integração

Tutorial prático da API Odoo REST com autenticação, operações CRUD, filtros de pesquisa, operações em lote e exemplos reais de Node.js e Python.

E
ECOSIRE Research and Development Team
|23 de março de 202616 min de leitura3.0k Palavras|

De acordo com o relatório State of APIs de 2025 do Postman, 73% das empresas agora integram seu ERP com pelo menos três sistemas externos. Odoo, que atende mais de 12 milhões de usuários em todo o mundo, expõe todo o seu modelo de dados por meio de uma rica camada de API.

De acordo com o relatório State of APIs de 2025 do Postman, 73% das empresas agora integram seu ERP com pelo menos três sistemas externos. Odoo, que atende mais de 12 milhões de usuários em todo o mundo, expõe todo o seu modelo de dados por meio de uma rica camada de API. No entanto, a documentação deixa muitos desenvolvedores lutando com fluxos de autenticação, operações em lote e tratamento de erros em nível de produção.

Este tutorial fornece exemplos prontos para copiar e colar em Python e Node.js para cada padrão de integração comum. Esteja você sincronizando pedidos do Shopify, enviando dados de um aplicativo móvel ou criando um painel personalizado, este guia irá ajudá-lo.

Principais conclusões

  • Odoo oferece três métodos de acesso à API: XML-RPC (legado, todas as versões), JSON-RPC (protocolo de cliente web) e API REST (Odoo 17+ com chaves de API) — cada um com autenticação e casos de uso diferentes.
  • Autenticação de chave de API (Odoo 17+) é a abordagem recomendada para integrações de servidor para servidor — sem gerenciamento de sessão, sem tokens CSRF, cabeçalhos HTTP diretos.
  • Domínios de pesquisa usam a poderosa notação polonesa do Odoo para filtragem. Domine os operadores e você poderá consultar qualquer combinação de dados.
  • Operações em lote são essenciais para o desempenho: a criação de 1.000 registros com uma chamada de API é 50 vezes mais rápida do que 1.000 chamadas individuais.
  • O tratamento de erros e a limitação de taxa são essenciais para integrações de produção — Odoo retorna respostas de erro estruturadas que seu código deve analisar e tratar normalmente.

1. Métodos de autenticação

Método 1: Chaves de API (recomendado para Odoo 17+)

As chaves de API são o método mais simples e seguro para comunicação entre servidores:

# Generate an API key in Odoo:
# Settings → Users → Select user → Account Security → New API Key

# Test authentication
curl -X GET "https://your-odoo.com/api/res.partner" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json"

Python com chave de API:

import requests

class OdooAPI:
    def __init__(self, url, api_key):
        self.url = url.rstrip('/')
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json',
        })

    def get(self, model, params=None):
        response = self.session.get(
            f'{self.url}/api/{model}',
            params=params or {}
        )
        response.raise_for_status()
        return response.json()

    def post(self, model, data):
        response = self.session.post(
            f'{self.url}/api/{model}',
            json=data
        )
        response.raise_for_status()
        return response.json()

# Usage
odoo = OdooAPI('https://your-odoo.com', 'your-api-key-here')
partners = odoo.get('res.partner', {'limit': 10})

Node.js com chave de API:

const axios = require('axios');

class OdooAPI {
  constructor(url, apiKey) {
    this.client = axios.create({
      baseURL: `${url}/api`,
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      timeout: 30000,
    });
  }

  async get(model, params = {}) {
    const { data } = await this.client.get(`/${model}`, { params });
    return data;
  }

  async post(model, body) {
    const { data } = await this.client.post(`/${model}`, body);
    return data;
  }

  async put(model, id, body) {
    const { data } = await this.client.put(`/${model}/${id}`, body);
    return data;
  }

  async delete(model, id) {
    const { data } = await this.client.delete(`/${model}/${id}`);
    return data;
  }
}

// Usage
const odoo = new OdooAPI('https://your-odoo.com', 'your-api-key');
const partners = await odoo.get('res.partner', { limit: 10 });

Método 2: JSON-RPC (todas as versões)

JSON-RPC é o protocolo usado internamente pelo cliente web do Odoo. Funciona com todas as versões do Odoo:

import requests
import json

class OdooJsonRpc:
    def __init__(self, url, db, username, password):
        self.url = url.rstrip('/')
        self.db = db
        self.session = requests.Session()
        self.uid = self._authenticate(username, password)

    def _authenticate(self, username, password):
        response = self._call('/web/session/authenticate', {
            'db': self.db,
            'login': username,
            'password': password,
        })
        if not response.get('uid'):
            raise Exception(f"Authentication failed: {response}")
        return response['uid']

    def _call(self, endpoint, params):
        payload = {
            'jsonrpc': '2.0',
            'method': 'call',
            'params': params,
            'id': None,
        }
        response = self.session.post(
            f'{self.url}{endpoint}',
            json=payload,
            headers={'Content-Type': 'application/json'}
        )
        result = response.json()
        if result.get('error'):
            raise Exception(result['error']['data']['message'])
        return result.get('result')

    def search_read(self, model, domain=None, fields=None, limit=80, offset=0, order=None):
        return self._call('/web/dataset/call_kw', {
            'model': model,
            'method': 'search_read',
            'args': [domain or []],
            'kwargs': {
                'fields': fields or [],
                'limit': limit,
                'offset': offset,
                'order': order or '',
            },
        })

    def create(self, model, values):
        return self._call('/web/dataset/call_kw', {
            'model': model,
            'method': 'create',
            'args': [values],
            'kwargs': {},
        })

    def write(self, model, ids, values):
        return self._call('/web/dataset/call_kw', {
            'model': model,
            'method': 'write',
            'args': [ids, values],
            'kwargs': {},
        })

# Usage
odoo = OdooJsonRpc('https://your-odoo.com', 'mydb', 'admin', 'password')
orders = odoo.search_read('sale.order', [('state', '=', 'sale')],
                          fields=['name', 'amount_total', 'partner_id'],
                          limit=20)

Método 3: XML-RPC (Legado, Universal)

import xmlrpc.client

url = 'https://your-odoo.com'
db = 'mydb'
username = 'admin'
password = 'password'

# Authenticate
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
uid = common.authenticate(db, username, password, {})

# Create models proxy
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')

# Search and read
partners = models.execute_kw(db, uid, password,
    'res.partner', 'search_read',
    [[('is_company', '=', True), ('country_id.code', '=', 'US')]],
    {'fields': ['name', 'email', 'phone'], 'limit': 10}
)

2. Operações CRUD

Criar registros

# Create a single contact
partner_id = odoo.create('res.partner', {
    'name': 'Acme Corporation',
    'is_company': True,
    'email': 'info@acme.com',
    'phone': '+1-555-0123',
    'street': '123 Main Street',
    'city': 'San Francisco',
    'state_id': 5,  # California
    'country_id': 233,  # United States
    'category_id': [(6, 0, [1, 3])],  # Tags: replace all with IDs 1 and 3
})

# Create a sale order with lines
order_id = odoo.create('sale.order', {
    'partner_id': partner_id,
    'date_order': '2026-03-23',
    'order_line': [
        (0, 0, {
            'product_id': 42,
            'product_uom_qty': 5,
            'price_unit': 99.99,
        }),
        (0, 0, {
            'product_id': 43,
            'product_uom_qty': 2,
            'price_unit': 149.99,
        }),
    ],
})

Equivalente em Node.js:

// Create a contact
const partnerId = await odoo.post('res.partner', {
  name: 'Acme Corporation',
  is_company: true,
  email: 'info@acme.com',
  phone: '+1-555-0123',
  street: '123 Main Street',
  city: 'San Francisco',
  country_id: 233,
});

// Create sale order with lines
const orderId = await odoo.post('sale.order', {
  partner_id: partnerId,
  date_order: '2026-03-23',
  order_line: [
    [0, 0, { product_id: 42, product_uom_qty: 5, price_unit: 99.99 }],
    [0, 0, { product_id: 43, product_uom_qty: 2, price_unit: 149.99 }],
  ],
});

Ler registros

# Read specific fields from specific records
data = odoo.search_read('sale.order',
    domain=[('state', '=', 'sale'), ('amount_total', '>', 500)],
    fields=['name', 'partner_id', 'amount_total', 'date_order', 'state'],
    limit=50,
    offset=0,
    order='date_order desc'
)

# Read a single record by ID (REST API)
# GET /api/sale.order/42?fields=name,amount_total
// Node.js — read with pagination
async function fetchAllOrders(odoo) {
  const pageSize = 100;
  let offset = 0;
  let allOrders = [];

  while (true) {
    const orders = await odoo.get('sale.order', {
      domain: JSON.stringify([['state', '=', 'sale']]),
      fields: 'name,partner_id,amount_total',
      limit: pageSize,
      offset,
      order: 'date_order desc',
    });

    allOrders = allOrders.concat(orders);
    if (orders.length < pageSize) break;
    offset += pageSize;
  }

  return allOrders;
}

Atualizar registros

# Update a single record
odoo.write('res.partner', [partner_id], {
    'phone': '+1-555-9999',
    'website': 'https://acme.com',
})

# Update multiple records at once
draft_orders = odoo.search_read('sale.order',
    [('state', '=', 'draft'), ('date_order', '<', '2026-01-01')],
    fields=['id']
)
ids = [o['id'] for o in draft_orders]
odoo.write('sale.order', ids, {'note': 'Reviewed Q1 2026'})

Excluir registros

# Delete records (use with caution)
odoo.write('res.partner', [obsolete_id], {'active': False})  # Prefer archiving

# Actually delete (rarely needed)
# models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[obsolete_id]])

3. Domínios de pesquisa avançada

Os domínios de pesquisa do Odoo usam notação polonesa (notação de prefixo) para combinar condições. O operador padrão entre condições é AND. Use barra vertical '|' para OR e e comercial '&' para AND explícito. Cada folha é uma tupla de nome de campo, operador e valor. Odoo suporta notação de ponto para filtrar campos de modelo relacionados, como 'partner_id.country_id.code' para filtrar pedidos por país do cliente.

# Complex domain examples

# Orders from US customers with amount > $1000, created this year
domain = [
    ('partner_id.country_id.code', '=', 'US'),
    ('amount_total', '>', 1000),
    ('date_order', '>=', '2026-01-01'),
    ('state', 'in', ['sale', 'done']),
]

# OR condition: email contains 'gmail' OR 'yahoo'
domain = [
    '|',
    ('email', 'ilike', 'gmail.com'),
    ('email', 'ilike', 'yahoo.com'),
]

# Complex: (state=sale AND amount>1000) OR (state=done AND amount>5000)
domain = [
    '|',
    '&', ('state', '=', 'sale'), ('amount_total', '>', 1000),
    '&', ('state', '=', 'done'), ('amount_total', '>', 5000),
]

# Negation: NOT archived
domain = [('active', '!=', False)]

# NULL check: has no email
domain = [('email', '=', False)]

# Hierarchical: all child categories of 'Electronics'
domain = [('categ_id', 'child_of', electronics_id)]

# Date ranges
from datetime import datetime, timedelta
domain = [
    ('create_date', '>=', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')),
    ('create_date', '<', datetime.now().strftime('%Y-%m-%d')),
]

Referência do Operador

OperadorDescriçãoExemplo
=Correspondência exata('state', '=', 'sale')
!=Diferente('state', '!=', 'cancelled')
>, >=, <, <=Comparação('amount', '>', 1000)
inValor na lista('state', 'in', ['sale', 'done'])
not inValor não listado('state', 'not in', ['cancel'])
likeSQL LIKE (diferencia maiúsculas de minúsculas)('name', 'like', 'Acme%')
ilikeLIKE sem distinção entre maiúsculas e minúsculas('email', 'ilike', '%@gmail.com')
=likeCorrespondência de padrões('ref', '=like', 'SO%')
child_ofDescendentes hierárquicos('categ_id', 'child_of', 1)
parent_ofAncestrais hierárquicos('categ_id', 'parent_of', 5)

4. Operações em lote

As operações em lote são essenciais para o desempenho. Nunca crie registros um de cada vez em um loop:

# BAD: 1000 API calls (slow, ~300 seconds)
for customer in customers:
    odoo.create('res.partner', customer)

# GOOD: 1 API call with batch (fast, ~3 seconds)
# Using JSON-RPC batch create
partner_ids = odoo._call('/web/dataset/call_kw', {
    'model': 'res.partner',
    'method': 'create',
    'args': [customers],  # Pass list of dicts
    'kwargs': {},
})
// Node.js batch pattern with chunking
async function batchCreate(odoo, model, records, chunkSize = 200) {
  const results = [];

  for (let i = 0; i < records.length; i += chunkSize) {
    const chunk = records.slice(i, i + chunkSize);
    console.log(`Creating ${model} batch ${i / chunkSize + 1}/${Math.ceil(records.length / chunkSize)}`);

    const ids = await odoo.post(model, chunk);
    results.push(...(Array.isArray(ids) ? ids : [ids]));

    // Respect rate limits
    if (i + chunkSize < records.length) {
      await new Promise(resolve => setTimeout(resolve, 500));
    }
  }

  return results;
}

// Usage
const customers = generateCustomerData(); // Array of 5000 objects
const ids = await batchCreate(odoo, 'res.partner', customers);
console.log(`Created ${ids.length} partners`);

5. Tratamento de erros

As integrações de produção devem lidar com erros normalmente:

import requests
import logging
import time

logger = logging.getLogger(__name__)

class OdooAPIClient:
    MAX_RETRIES = 3
    RETRY_DELAY = 2  # seconds

    def __init__(self, url, api_key):
        self.url = url.rstrip('/')
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json',
        })

    def _request(self, method, endpoint, **kwargs):
        last_error = None

        for attempt in range(self.MAX_RETRIES):
            try:
                response = self.session.request(
                    method,
                    f'{self.url}{endpoint}',
                    timeout=30,
                    **kwargs
                )

                if response.status_code == 429:
                    retry_after = int(response.headers.get('Retry-After', 60))
                    logger.warning(f"Rate limited. Retrying after {retry_after}s")
                    time.sleep(retry_after)
                    continue

                if response.status_code == 401:
                    raise AuthenticationError("Invalid API key or session expired")

                if response.status_code == 403:
                    raise PermissionError(f"Access denied: {response.text}")

                if response.status_code == 404:
                    raise RecordNotFoundError(f"Record not found: {endpoint}")

                if response.status_code >= 500:
                    logger.error(f"Server error {response.status_code}: {response.text}")
                    if attempt < self.MAX_RETRIES - 1:
                        time.sleep(self.RETRY_DELAY * (attempt + 1))
                        continue
                    raise ServerError(f"Server error after {self.MAX_RETRIES} retries")

                response.raise_for_status()
                return response.json()

            except requests.exceptions.ConnectionError as e:
                last_error = e
                logger.warning(f"Connection error (attempt {attempt + 1}): {e}")
                if attempt < self.MAX_RETRIES - 1:
                    time.sleep(self.RETRY_DELAY * (attempt + 1))

            except requests.exceptions.Timeout as e:
                last_error = e
                logger.warning(f"Request timeout (attempt {attempt + 1}): {e}")
                if attempt < self.MAX_RETRIES - 1:
                    time.sleep(self.RETRY_DELAY * (attempt + 1))

        raise ConnectionError(f"Failed after {self.MAX_RETRIES} attempts: {last_error}")

class AuthenticationError(Exception): pass
class PermissionError(Exception): pass
class RecordNotFoundError(Exception): pass
class ServerError(Exception): pass
// Node.js error handling with axios interceptors
const axios = require('axios');

function createOdooClient(url, apiKey) {
  const client = axios.create({
    baseURL: `${url}/api`,
    headers: { Authorization: `Bearer ${apiKey}` },
    timeout: 30000,
  });

  // Response interceptor for error handling
  client.interceptors.response.use(
    (response) => response,
    async (error) => {
      const { config, response } = error;
      config.__retryCount = config.__retryCount || 0;

      // Rate limiting
      if (response?.status === 429) {
        const retryAfter = parseInt(response.headers['retry-after'] || '60', 10);
        console.warn(`Rate limited. Waiting ${retryAfter}s...`);
        await new Promise((r) => setTimeout(r, retryAfter * 1000));
        return client(config);
      }

      // Retry on server errors (max 3)
      if (response?.status >= 500 && config.__retryCount < 3) {
        config.__retryCount += 1;
        const delay = config.__retryCount * 2000;
        console.warn(`Server error ${response.status}. Retry ${config.__retryCount}/3 in ${delay}ms`);
        await new Promise((r) => setTimeout(r, delay));
        return client(config);
      }

      // Structured error response
      const errorMessage = response?.data?.error?.message
        || response?.data?.message
        || error.message;

      throw new Error(`Odoo API Error [${response?.status}]: ${errorMessage}`);
    }
  );

  return client;
}

6. Exemplos de integração no mundo real

Sincronização de pedidos do Shopify para Odoo

class ShopifyOdooSync:
    def __init__(self, odoo_client, shopify_client):
        self.odoo = odoo_client
        self.shopify = shopify_client

    def sync_order(self, shopify_order):
        # 1. Find or create customer
        partner = self._get_or_create_partner(shopify_order['customer'])

        # 2. Map products
        order_lines = []
        for item in shopify_order['line_items']:
            product_id = self._find_product_by_sku(item['sku'])
            if not product_id:
                logger.warning(f"Product not found for SKU: {item['sku']}")
                continue
            order_lines.append((0, 0, {
                'product_id': product_id,
                'product_uom_qty': item['quantity'],
                'price_unit': float(item['price']),
                'discount': self._calc_discount(item),
            }))

        # 3. Create sale order
        order_id = self.odoo.create('sale.order', {
            'partner_id': partner['id'],
            'client_order_ref': shopify_order['name'],  # Shopify order #
            'order_line': order_lines,
            'note': f"Shopify Order: {shopify_order['id']}",
        })

        # 4. Auto-confirm if paid
        if shopify_order['financial_status'] == 'paid':
            self.odoo._call('/web/dataset/call_kw', {
                'model': 'sale.order',
                'method': 'action_confirm',
                'args': [[order_id]],
                'kwargs': {},
            })

        return order_id

    def _get_or_create_partner(self, customer):
        # Search by email first
        existing = self.odoo.search_read('res.partner',
            [('email', '=', customer['email'])],
            fields=['id', 'name'], limit=1)

        if existing:
            return existing[0]

        return {'id': self.odoo.create('res.partner', {
            'name': f"{customer['first_name']} {customer['last_name']}",
            'email': customer['email'],
            'phone': customer.get('phone'),
        })}

    def _find_product_by_sku(self, sku):
        products = self.odoo.search_read('product.product',
            [('default_code', '=', sku)],
            fields=['id'], limit=1)
        return products[0]['id'] if products else None

Receptor Webhook (exemplo de frasco)

from flask import Flask, request, jsonify
import hmac
import hashlib

app = Flask(__name__)

@app.route('/webhook/odoo/order', methods=['POST'])
def handle_odoo_webhook():
    # Verify signature
    signature = request.headers.get('X-Odoo-Signature')
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        request.data,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        return jsonify({'error': 'Invalid signature'}), 401

    payload = request.json
    event_type = payload.get('event')

    if event_type == 'sale.order.confirmed':
        handle_order_confirmed(payload['data'])
    elif event_type == 'stock.picking.done':
        handle_shipment_complete(payload['data'])

    return jsonify({'status': 'ok'}), 200

7. Dicas de desempenho

DicaImpactoDetalhes
Use o parâmetro fieldsAltoSolicite apenas os campos necessários - reduz a carga útil de 5 a 10 vezes
Criações em loteAlto1 chamada com 500 registros versus 500 chamadas — 50x mais rápido
Paginar grandes conjuntos de dadosMédioUse limit e offset — evite carregar 100 mil registros
Dados somente leitura em cacheMédioCache de catálogos de produtos, categorias (TTL 5-15 min)
Usar search_countBaixoConte antes de buscar — evite carregar dados apenas para contar
Pool de conexõesMédioReutilize sessões HTTP — economiza sobrecarga de handshake TLS

Perguntas frequentes

Qual é a diferença entre XML-RPC, JSON-RPC e API REST no Odoo?

XML-RPC é o protocolo legado disponível em todas as versões do Odoo — é detalhado, mas tem suporte universal. JSON-RPC é o protocolo usado pelo cliente web do Odoo e fornece a mesma funcionalidade com cargas JSON. A API REST foi introduzida no Odoo 17 e fornece endpoints HTTP padrão com autenticação de chave de API, tornando-a a opção mais fácil para integrações modernas. Para novos projetos, use a API REST se você estiver no Odoo 17 ou posterior.

Como faço para lidar com os limites de taxa da API Odoo?

Odoo.sh aplica limites de taxa com base no nível do seu plano. Ao receber uma resposta 429, leia o cabeçalho Retry-After e aguarde antes de tentar novamente. Para integrações de alto volume, implemente a espera exponencial, agrupe suas operações para reduzir o número de chamadas de API e considere usar as ações programadas do Odoo para processamento em massa, em vez de chamadas de API em tempo real para sincronizações não críticas.

Posso chamar métodos Python personalizados por meio da API?

Sim. Qualquer método público em um modelo Odoo pode ser chamado através de XML-RPC ou JSON-RPC usando execute_kw. Para a API REST, você precisa criar um endpoint de controlador personalizado com @http.route. Os métodos que começam com sublinhado são privados e não podem ser chamados externamente através de XML-RPC. Sempre valide as entradas em seus métodos personalizados para evitar ataques de injeção.

Como sincronizo grandes conjuntos de dados com eficiência?

Use uma combinação de estratégias: sincronização completa inicial com operações em lote e paginação (limite de 200 registros por solicitação) e, em seguida, sincronizações incrementais usando a filtragem write_date para buscar apenas os registros modificados desde a última sincronização. Armazene o carimbo de data/hora da última sincronização e use-o como filtro de domínio. Para conjuntos de dados muito grandes que excedem 100.000 registros, considere a replicação direta do banco de dados em vez da sincronização por API.

A API REST do Odoo está disponível no Odoo Community Edition?

A API REST nativa com autenticação de chave de API foi introduzida no Odoo 17 Enterprise. Para a comunidade Odoo, você pode usar XML-RPC ou JSON-RPC, que estão disponíveis em todas as edições, ou instalar módulos da comunidade, como o rest-framework do OCA, que adiciona endpoints RESTful. Os serviços de integração do ECOSIRE suportam todas as edições Odoo e protocolos API.

Como lidar com os campos Many2many e One2many em chamadas de API?

Os campos relacionais usam tuplas de comandos especiais: (0, 0, valores) para criar e vincular um novo registro, (1, id, valores) para atualizar um registro vinculado, (2, id, 0) para excluir um registro vinculado, (3, id, 0) para desvincular sem excluir, (4, id, 0) para vincular um registro existente, (5, 0, 0) para desvincular todos e (6, 0, [ids]) para substituir todos os links. Para leitura, esses campos retornam listas de IDs por padrão. Use search_read com o nome do campo para obter dados completos.

Próximas etapas

A integração de API é a espinha dorsal dos sistemas empresariais modernos. Esteja você construindo uma sincronização de dados simples ou uma orquestração multiplataforma complexa, os padrões neste guia serão úteis para você.

Recursos relacionados:

Precisa de ajuda com a integração da API Odoo? A equipe de integração da ECOSIRE conectou o Odoo a mais de 50 plataformas externas, incluindo Shopify, Amazon, Salesforce e ERPs personalizados. Desde simples sincronizações de dados até orquestração bidirecional em tempo real, construímos integrações escalonáveis. Agende uma consulta técnica gratuita.

E

Escrito por

ECOSIRE Research and Development Team

Redação técnica

A equipe de redação técnica da ECOSIRE escreve sobre Odoo ERP, e-commerce com Shopify, agentes de IA, análise de dados com Power BI, automação com GoHighLevel e boas práticas de software empresarial. Nossos guias ajudam as empresas a tomar decisões de tecnologia bem fundamentadas.

ECOSIRE

Transforme seu negócio com o Odoo ERP

Implementação, personalização e suporte especializado do Odoo para agilizar suas operações.