Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laradoo Laravel Package

edujugon/laradoo

Laradoo is a Laravel package for integrating Odoo ERP. It provides an Eloquent-style query builder and models to query, create, update, and delete Odoo records via XML-RPC/JSON-RPC, making ERP data access feel native in Laravel applications.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish config

composer require edujugon/laradoo
php artisan vendor:publish --provider="Edujugon\Laradoo\Providers\LaradooServiceProvider"

On Laravel ≤5.4, register the provider and the Laradoo alias manually in config/app.php; on 5.5+ package discovery handles it.

2. Fill config/laradoo.php (or .env)

ODOO_HOST=erp.example.com
ODOO_PORT=8069
ODOO_DB=production
ODOO_USERNAME=api-user
ODOO_PASSWORD=secret
ODOO_PROTOCOL=xmlrpc   # or jsonrpc

3. Sanity-check connectivity first

Before writing any business code, prove auth works and inspect the shape of the payloads:

use Edujugon\Laradoo\Facades\Laradoo;

$version = Laradoo::version();       // ['server_version' => '14.0', ...]
$fields  = Laradoo::model('res.partner')->fields();

If version() throws, you have a host/port/db/credentials/protocol problem — not a Laravel problem. Fix that first.

4. First real use case — a read

$customers = Laradoo::model('res.partner')
    ->where('is_company', '=', true)
    ->limit(20)
    ->get(['name', 'email', 'country_id']);

Where to look in the repo: config/laradoo.php (all connection knobs), src/Odoo.php (the chainable API surface — this is the real documentation), src/Facades/Laradoo.php, and the README for the handful of worked examples. Because the package is thin, the class itself is the most reliable reference for method signatures.


Implementation Patterns

Treat model() as the start of every chain

The facade is a singleton wrapping a mutable query state. Open each logical operation by naming the model, then chain domain, pagination, and projection:

Laradoo::model('sale.order')
    ->where('state', 'in', ['sale', 'done'])
    ->where('date_order', '>=', '2024-01-01')
    ->orderBy('date_order', 'desc')
    ->limit(50)
    ->offset(100)
    ->get(['name', 'partner_id', 'amount_total', 'date_order']);

Wrap it in a repository / service class

Never sprinkle Laradoo:: calls through controllers and Blade. Define an interface so you can mock Odoo in tests (the XML-RPC client uses cURL, so Http::fake() will not help you):

interface OdooGateway {
    public function partners(array $filters = []): array;
    public function createOrder(array $data): int;
}

class LaradooGateway implements OdooGateway {
    public function partners(array $filters = []): array {
        return Laradoo::model('res.partner')
            ->where('is_company', '=', true)
            ->get(['name', 'email']);
    }
}

Paginate with count() + limit()/offset()

Odoo returns plain arrays, so build your own paginator:

$total = Laradoo::model('res.partner')->count();
$rows  = Laradoo::model('res.partner')
    ->limit(25)->offset(($page - 1) * 25)->get();

Writes: create → update → delete, always by id

$id = Laradoo::model('res.partner')->create([
    'name'     => 'ACME Ltd',
    'is_company'=> true,
    'email'    => '[email protected]',
]);

Laradoo::model('res.partner')->update($id, ['phone' => '+1 555 0100']);

Laradoo::model('res.partner')->delete($id);

Odoo writes return only the affected id(s) — re-find($id) if you need the persisted record back.

Push writes into queued jobs

XML-RPC is synchronous and blocking; Odoo can be slow on large models. Put create/update/delete behind a job with retries/backoff so a slow ERP doesn't tie up HTTP requests:

class SyncCustomerToOdoo implements ShouldQueue {
    public int $tries = 3;
    public function handle(): void {
        Laradoo::model('res.partner')->create($this->payload);
    }
}

Use find() for detail pages, searchRead for hot paths

find($id) is a clean single-record read. For anything list-shaped, the raw search_read($domain, $fields, $offset, $limit, $order) call lets you skip chain state entirely — useful inside loops and jobs where you don't want leakage between iterations.

Reports

If you need PDFs (invoices, delivery slips), the package exposes a report() call against a report name and record ids. The payload comes back base64-encoded; decode it server-side and stream it to the user rather than returning the raw string to the browser.

Multi-tenant / multi-database

There is no first-class "connections" concept. Either reconfigure config(['laradoo.database' => $db]) before each call, or resolve a fresh Odoo instance with an explicit config array. Never rely on the facade singleton when two Odoo databases are in play in one request.


Gotchas and Tips

The package is frozen at 2020. Its composer constraints target Laravel 6/7-era dependencies. On Laravel 9+ you will likely need --with-all-dependencies and may have to relax constraints or fork; pin phpxmlrpc/phpxmlrpc to a 4.x release that runs on PHP 8, since older versions have fatal incompatibilities.

No Http client under the hood. Everything goes through cURL/XML-RPC (or the JSON-RPC endpoint). That means: no Http::fake() in tests, no Laravel HTTP middleware, no automatic retry, and no built-in timeout you can configure from laradoo.php. If your firewall or proxy drops the connection on long-running calls, wrap the call in try/catch and set a cURL timeout manually.

Credentials and protocol mismatches are the #1 error source. Odoo 12+ exposes XML-RPC at /xmlrpc/2/ and JSON-RPC at /jsonrpc. If you flip protocol without confirming the server, you get cryptic "wrong version" or 404s from the underlying client, not from Laradoo. Also confirm the database name exactly — Odoo is case-sensitive and a bad DB name returns the same auth error as a bad password.

Odoo errors are nested dictionaries. When a domain references an invalid field, an ACL denies a write, or a required field is missing, the exception message contains a Python traceback (Fault, odoo.exceptions.AccessError, ValidationError). Catch \Exception, log the full message, and surface a generic error to users. Do not display $e->getMessage() to customers.

Many2one fields round-trip asymmetrically. Reads return [id, "Display Name"]; writes want just the integer id. If you pass the array back into update(), Odoo either silently ignores it or writes garbage.

One2many / Many2many need command tuples. To set lines on an order, use Odoo's (0, 0, {...}), (4, id), (6, 0, [ids]) syntax inside the payload. A plain array of ids will fail validation.

Dates are naive UTC strings. Odoo always returns Y-m-d H:i:s in UTC regardless of the user's timezone. Cast with Carbon::parse($v, 'UTC') and convert at the edge.

Field names must be literal. Odoo has no select * — get() without a field list can be slow and returns dozens of columns. Use fields() once, offline, to discover names (partner_id, amount_total, x_studio_* for custom fields), then hard-code projections.

Chain state can leak in long-running workers. The facade returns a mutated singleton. In queue workers, Octane, or any loop, begin each iteration by re-calling model(...) and avoid reusing a previously built chain — or resolve a fresh Laradoo instance per unit of work.

Access rights are enforced server-side. A user that can log into the web UI may still fail over the API if the record rules or model ACLs differ. Give the integration a dedicated Odoo user with the minimal groups it needs (Sales / User, Inventory / User, etc.), and test each model you call with fields() first — that call only succeeds if the user can read the model.

fields() is expensive. It returns the full metadata dictionary for a model (hundreds of entries). Cache it or avoid it in the request path entirely.

Bulk writes have no transaction. There is no exposed multi-call transaction. If you're importing 500 records, loop with batching, log each id returned, and make the job idempotent (search by an external reference field before create) so retries don't duplicate records.

Version check on boot. Wire Laradoo::version() into a health-check command (php artisan odoo:ping). It's the single cheapest way to catch credential rotation, renamed databases, or a protocol change before users hit a 500.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package