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, ]; } }