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>
This commit is contained in:
259
includes/class-woodoo-api.php
Normal file
259
includes/class-woodoo-api.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user