Files
WooDoo/includes/class-woodoo-api.php
Malin 68c1ff4455 feat: initial WooDoo plugin – WooCommerce & Odoo 19 integration
- Odoo JSON-RPC client (no Composer, uses wp_remote_post)
- Admin settings page under WooCommerce with connection test
- Customer linking: search Odoo partners from WP user profile
- My Account: Odoo Invoices tab with PDF proxy download
- My Account: Book a Meeting tab (slot calculator + calendar.event)
- WC order → Odoo sale.order auto-sync on processing status
- Products matched by SKU; partner auto-created from billing info
- Uninstall cleanup (options, user meta, order meta, DB table)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:58:27 +02:00

260 lines
8.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Odoo JSON-RPC API client.
*
* Communicates with Odoo 19 via the /jsonrpc endpoint using the
* "common" (authenticate) and "object" (execute_kw) services.
* Uses WordPress's built-in HTTP API no Composer required.
*/
defined( 'ABSPATH' ) || exit;
class WooDoo_API {
private string $url;
private string $db;
private string $username;
private string $api_key;
private ?int $uid = null;
/** Cache results for one minute to reduce round-trips */
private const CACHE_TTL = 60;
public function __construct(
string $url,
string $db,
string $username,
string $api_key
) {
$this->url = $url;
$this->db = $db;
$this->username = $username;
$this->api_key = $api_key;
}
// ── Low-level JSON-RPC ────────────────────────────────────────────────
/**
* POST to /jsonrpc.
*
* @param string $service "common" | "object"
* @param string $method e.g. "authenticate" | "execute_kw"
* @param array $args positional arguments
* @return mixed decoded result or WP_Error
*/
public function jsonrpc( string $service, string $method, array $args ): mixed {
$body = wp_json_encode( [
'jsonrpc' => '2.0',
'method' => 'call',
'id' => wp_rand( 1, 999999999 ),
'params' => [
'service' => $service,
'method' => $method,
'args' => $args,
],
] );
$response = wp_remote_post(
$this->url . '/jsonrpc',
[
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => $body,
'timeout' => 30,
'sslverify' => apply_filters( 'woodoo_ssl_verify', true ),
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $data['error'] ) ) {
$msg = $data['error']['data']['message']
?? $data['error']['message']
?? 'Unknown Odoo error';
return new WP_Error( 'odoo_error', $msg, $data['error'] );
}
return $data['result'] ?? null;
}
// ── Authentication ────────────────────────────────────────────────────
/**
* Authenticate and cache the uid for this request lifecycle.
*/
public function authenticate(): ?int {
if ( $this->uid ) return $this->uid;
$result = $this->jsonrpc( 'common', 'authenticate', [
$this->db,
$this->username,
$this->api_key,
[],
] );
if ( is_wp_error( $result ) || ! is_int( $result ) || $result <= 0 ) {
return null;
}
$this->uid = $result;
return $this->uid;
}
// ── ORM execute_kw wrapper ────────────────────────────────────────────
/**
* Call any ORM method via execute_kw.
*
* @param string $model e.g. 'res.partner'
* @param string $method e.g. 'search_read'
* @param array $args positional args (list of lists usually)
* @param array $kwargs keyword args (fields, limit, offset, etc.)
*/
public function execute_kw( string $model, string $method, array $args = [], array $kwargs = [] ): mixed {
$uid = $this->authenticate();
if ( ! $uid ) {
return new WP_Error( 'woodoo_auth', 'Could not authenticate with Odoo.' );
}
return $this->jsonrpc( 'object', 'execute_kw', [
$this->db,
$uid,
$this->api_key,
$model,
$method,
$args,
$kwargs,
] );
}
// ── Convenience helpers ───────────────────────────────────────────────
public function search_read(
string $model,
array $domain = [],
array $fields = [],
int $limit = 0,
int $offset = 0,
string $order = ''
): array {
$kwargs = [ 'fields' => $fields ];
if ( $limit > 0 ) $kwargs['limit'] = $limit;
if ( $offset > 0 ) $kwargs['offset'] = $offset;
if ( $order !== '' ) $kwargs['order'] = $order;
$result = $this->execute_kw( $model, 'search_read', [ $domain ], $kwargs );
return is_array( $result ) ? $result : [];
}
public function search(
string $model,
array $domain = [],
int $limit = 0,
int $offset = 0,
string $order = ''
): array {
$kwargs = [];
if ( $limit > 0 ) $kwargs['limit'] = $limit;
if ( $offset > 0 ) $kwargs['offset'] = $offset;
if ( $order !== '' ) $kwargs['order'] = $order;
$result = $this->execute_kw( $model, 'search', [ $domain ], $kwargs );
return is_array( $result ) ? $result : [];
}
public function read( string $model, array $ids, array $fields = [] ): array {
$kwargs = $fields ? [ 'fields' => $fields ] : [];
$result = $this->execute_kw( $model, 'read', [ $ids ], $kwargs );
return is_array( $result ) ? $result : [];
}
public function create( string $model, array $values ): ?int {
$result = $this->execute_kw( $model, 'create', [ $values ] );
return is_int( $result ) ? $result : null;
}
public function write( string $model, array $ids, array $values ): bool {
$result = $this->execute_kw( $model, 'write', [ $ids, $values ] );
return $result === true;
}
public function search_count( string $model, array $domain = [] ): int {
$result = $this->execute_kw( $model, 'search_count', [ $domain ] );
return is_int( $result ) ? $result : 0;
}
public function unlink( string $model, array $ids ): bool {
$result = $this->execute_kw( $model, 'unlink', [ $ids ] );
return $result === true;
}
// ── Partner helpers ───────────────────────────────────────────────────
/**
* Find a partner by email or create one.
* Returns the Odoo partner ID.
*/
public function find_or_create_partner( string $email, string $name, array $extra = [] ): ?int {
$found = $this->search( 'res.partner', [ [ 'email', '=', $email ] ], 1 );
if ( ! empty( $found ) ) {
return (int) $found[0];
}
return $this->create( 'res.partner', array_merge( [
'name' => $name,
'email' => $email,
], $extra ) );
}
// ── Product helpers ───────────────────────────────────────────────────
/**
* Find a product.product by its SKU (default_code).
* Returns product ID or null if not found.
*/
public function find_product_by_sku( string $sku ): ?int {
if ( empty( $sku ) ) return null;
$found = $this->search( 'product.product', [ [ 'default_code', '=', $sku ] ], 1 );
return ! empty( $found ) ? (int) $found[0] : null;
}
// ── Diagnostics ───────────────────────────────────────────────────────
/**
* Test connectivity and credentials.
* Returns ['success' => bool, 'message' => string, 'version' => string|null]
*/
public function test_connection(): array {
// Version check (no auth needed)
$version = $this->jsonrpc( 'common', 'version', [] );
if ( is_wp_error( $version ) ) {
return [
'success' => false,
'message' => 'Cannot reach Odoo: ' . $version->get_error_message(),
'version' => null,
];
}
$ver_str = $version['server_version'] ?? 'unknown';
// Try authenticate
$uid = $this->authenticate();
if ( ! $uid ) {
return [
'success' => false,
'message' => "Reached Odoo {$ver_str} but authentication failed. Check username / API key.",
'version' => $ver_str,
];
}
return [
'success' => true,
'message' => "Connected to Odoo {$ver_str} as UID {$uid}.",
'version' => $ver_str,
];
}
}