This package was developed to give you a quick start to communicate with the Odoo external API from Laravel. It wraps the most common endpoints — sessions, users, employees, projects, tasks and timesheets — behind a clean, typed connector built on Saloon.
- What is Odoo?
- Requirements
- Installation
- Configuration
- Basic Usage
- API Reference
- DTOs
- Testing
- Changelog
- Contributing
- Security Vulnerabilities
- Credits
- License
Odoo is an open-source suite of business applications covering CRM, sales, project management, timesheets, accounting, inventory and more. It exposes an external API that lets you read and write records across all of these modules. This package provides a typed, Laravel-friendly client for the most common Odoo endpoints used in day-to-day integrations.
| Package | PHP | Laravel |
|---|---|---|
| v1.0.0 | ^8.4 | ^13.0 |
You can install the package via composer:
composer require codebar-ag/laravel-odooOptionally publish the config file to adjust defaults:
php artisan vendor:publish --provider="CodebarAg\Odoo\OdooServiceProvider" --tag="laravel-odoo-config"You can generate an API key in your Odoo user profile under Preferences → API Keys.
Add the following variables to your .env file:
LARAVEL_ODOO_URL=https://your-odoo-instance.com
LARAVEL_ODOO_API_KEY=your-api-key
LARAVEL_ODOO_DB=your-database
LARAVEL_ODOO_TIMEOUT=15 # optional — request timeout in seconds (default 15, 0 = no timeout)
LARAVEL_ODOO_MAX_REDIRECTS=5 # optional — max HTTP redirects to follow (default 5)Create an OdooConnector instance with your Odoo URL, API key, and optionally a database name:
use CodebarAg\Odoo\OdooConnector;
$connector = new OdooConnector(
baseUrl: 'https://your-odoo-instance.com',
apiKey: 'your-api-key',
db: 'your-database', // optional
maxRedirects: 5, // optional — max HTTP redirects to follow (default 5)
timeout: 15.0, // optional — request timeout in seconds (default 15, 0 = no timeout)
);Each method returns a typed response object with dedicated methods for accessing the data.
If you set the environment variables above (or publish and edit the config file), the package binds a pre-configured OdooConnector in the container, so you can resolve it or use the Odoo facade instead of constructing it by hand:
use CodebarAg\Odoo\Facades\Odoo;
$response = Odoo::health();
$response->isHealthy(); // boolThe facade reads url, api_key, db, timeout, and max_redirects from config/laravel-odoo.php (configurable via their respective LARAVEL_ODOO_* env vars). Direct instantiation with new OdooConnector(...) remains fully supported — for example when you need to talk to more than one Odoo instance.
// Check if the Odoo instance is reachable
$response = $connector->health();
$response->isHealthy(); // bool
// Get the Odoo server version
$response = $connector->version();
$response->serverVersion(); // ?string e.g. "17"
$response->serie(); // ?string e.g. "17.0"
// List all available databases
$response = $connector->databases();
$response->databases(); // array<string>// Get the currently authenticated user
$response = $connector->getUser(
fields: ['name', 'email'], // optional — omit to get the default field set
domain: [], // optional
limit: 1, // optional, default 1
);
$user = $response->dto(); // ?UserDto (id, name, lang)
// Get a user by their Odoo ID
$response = $connector->getUserById(
uid: 1,
fields: ['name', 'email'], // optional
);
// Get the authenticated user's context (timezone, language)
$response = $connector->getUserContext();// Get an employee by their Odoo user ID
$response = $connector->getEmployeeByUserId(
userId: 1,
fields: ['name', 'job_title'], // optional — omit to get all fields
limit: 1, // optional, default 1
);
$response->dto(); // ?EmployeeDto// Get fields for a specific model
$response = $connector->getFields(
model: 'account.move',
attributes: ['string', 'type'], // optional — field meta-attributes to return
);
$response->fields(); // array<string, FieldDto>
// Get all fields across all models
$response = $connector->getAllFields();
$response->fields(); // array<string, FieldDto>// Check permissions for a model and operation
$response = $connector->getPermissions(
model: 'project.project',
operation: 'read', // read, write, create, unlink
);
$response->allowed(); // bool$response = $connector->getProjects(
fields: ['name', 'date_start', 'date'], // optional
domain: [['active', '=', true]], // optional Odoo domain filter
limit: 100, // optional, default 100
);
/** @var array<ProjectDto> $projects */
$projects = $response->projects();// Get all tasks
$response = $connector->getAllTasks(
fields: ['name', 'project_id', 'stage_id'], // optional
domain: [['active', '=', true]], // optional
limit: 100, // optional, default 100
);
/** @var array<TaskDto> $tasks */
$tasks = $response->tasks();
// Get tasks for a specific project
$response = $connector->getTasksByProject(
projectId: 42,
fields: ['name', 'stage_id', 'date_deadline'], // optional
limit: 100, // optional, default 100
operator: '=', // optional domain operator, default '='
);
/** @var array<TaskDto> $tasks */
$tasks = $response->tasks();use CodebarAg\Odoo\Dto\Timesheets\CreateTimesheetDto;
use CodebarAg\Odoo\Dto\Timesheets\UpdateTimesheetDto;
// Get timesheet entries
$response = $connector->getTimesheetEntries(
fields: ['name', 'project_id', 'task_id', 'unit_amount', 'date'], // optional
domain: [['employee_id', '=', 5]], // optional
limit: 100, // optional
);
/** @var array<TimesheetEntryDto> $entries */
$entries = $response->entries();
// Get timesheet entries from the last N days
$response = $connector->getTimesheetEntriesLastDays(
days: 7,
fields: ['name', 'date', 'unit_amount'], // optional
operator: '>=', // optional domain operator, default '>='
);
$entries = $response->entries(); // array<TimesheetEntryDto>
// Read a single timesheet entry
$response = $connector->readTimesheet(id: 123);
$entry = $response->dto(); // ?TimesheetEntryDto
// Create a timesheet entry
$response = $connector->createTimesheet(new CreateTimesheetDto(
name: 'Fixed bug #456',
projectId: 1,
taskId: 10,
date: '2024-06-11',
unitAmount: 1.5,
employeeId: 5, // optional
extraValues: [], // optional — extra Odoo fields (e.g. custom Studio fields)
));
$newId = $response->id(); // ?int
// Update a timesheet entry
$response = $connector->updateTimesheet(new UpdateTimesheetDto(
id: 123,
values: ['name' => 'Updated description', 'unit_amount' => 2.0],
));
$response->ok(); // bool
// Delete a timesheet entry
$response = $connector->deleteTimesheet(id: 123);
$response->ok(); // boolFetch projects, all tasks, and all timesheet entries in one call:
$results = $connector->syncAll();
$projects = $results['projects']->projects(); // array<ProjectDto>
$tasks = $results['tasks']->tasks(); // array<TaskDto>
$timesheets = $results['timesheets']->entries(); // array<TimesheetEntryDto>Read DTOs are built on spatie/laravel-data.
Odoo's relation tuples ([id, name]) are flattened onto paired properties
(e.g. projectId / projectName) and its false-means-empty sentinel is
normalised to null. Each DTO keeps a fromArray() factory for backwards
compatibility and is also a full laravel-data Data object (from(), collect(), …).
| DTO | Description |
|---|---|
ProjectDto |
Represents an Odoo project |
TaskDto |
Represents an Odoo task |
TimesheetEntryDto |
Represents a timesheet entry (read) |
CreateTimesheetDto |
Payload for creating a timesheet entry |
UpdateTimesheetDto |
Payload for updating a timesheet entry |
EmployeeDto |
Represents an Odoo employee |
UserDto |
Represents the authenticated Odoo user |
FieldDto |
Represents a field definition on an Odoo model |
composer testFor live integration tests against a real Odoo instance, copy phpunit.xml.dist to phpunit.xml, fill in the LARAVEL_ODOO_URL, LARAVEL_ODOO_API_KEY and LARAVEL_ODOO_DB env values, then run:
composer test:livePlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
- Sebastian Bürgin-Fix
- Tobias Brogle
- All Contributors
- Skeleton Repository from Spatie
- Laravel Package Training from Spatie
The MIT License (MIT). Please see License File for more information.
