- 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>
220 lines
8.2 KiB
PHP
220 lines
8.2 KiB
PHP
<?php
|
||
/**
|
||
* My Account – Invoices tab.
|
||
* Shows the customer's Odoo invoices and handles PDF proxy-download.
|
||
*/
|
||
|
||
defined( 'ABSPATH' ) || exit;
|
||
|
||
class WooDoo_Invoices {
|
||
|
||
const ENDPOINT = 'odoo-invoices';
|
||
|
||
public static function init(): void {
|
||
add_filter( 'woocommerce_account_menu_items', [ __CLASS__, 'add_menu_item' ], 20 );
|
||
add_action( 'woocommerce_account_' . self::ENDPOINT . '_endpoint', [ __CLASS__, 'render' ] );
|
||
add_filter( 'woocommerce_get_query_vars', [ __CLASS__, 'add_query_var' ] );
|
||
|
||
// PDF download endpoint
|
||
add_action( 'wp_ajax_woodoo_invoice_pdf', [ __CLASS__, 'ajax_download_pdf' ] );
|
||
add_action( 'wp_ajax_nopriv_woodoo_invoice_pdf', [ __CLASS__, 'ajax_download_pdf' ] );
|
||
}
|
||
|
||
public static function add_query_var( array $vars ): array {
|
||
$vars[ self::ENDPOINT ] = self::ENDPOINT;
|
||
return $vars;
|
||
}
|
||
|
||
public static function add_menu_item( array $items ): array {
|
||
$new = [];
|
||
foreach ( $items as $key => $label ) {
|
||
$new[ $key ] = $label;
|
||
if ( $key === 'orders' ) {
|
||
$new[ self::ENDPOINT ] = __( 'Odoo Invoices', 'woodoo' );
|
||
}
|
||
}
|
||
return $new;
|
||
}
|
||
|
||
// ── Render ────────────────────────────────────────────────────────────
|
||
|
||
public static function render(): void {
|
||
$user_id = get_current_user_id();
|
||
$partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true );
|
||
|
||
if ( ! $partner_id ) {
|
||
echo '<p class="woodoo-notice">' .
|
||
esc_html__( 'Your account is not yet linked to Odoo. Please contact us.', 'woodoo' ) .
|
||
'</p>';
|
||
return;
|
||
}
|
||
|
||
$api = woodoo_api();
|
||
if ( ! $api ) {
|
||
echo '<p class="woodoo-notice woodoo-error">' .
|
||
esc_html__( 'Odoo integration is not configured. Please contact support.', 'woodoo' ) .
|
||
'</p>';
|
||
return;
|
||
}
|
||
|
||
// Page parameter
|
||
$paged = max( 1, (int) ( $_GET['invoice_page'] ?? 1 ) ); // phpcs:ignore
|
||
$per_page = 10;
|
||
$offset = ( $paged - 1 ) * $per_page;
|
||
|
||
$domain = [
|
||
[ 'partner_id', '=', $partner_id ],
|
||
[ 'move_type', '=', 'out_invoice' ],
|
||
[ 'state', '!=', 'cancel' ],
|
||
];
|
||
|
||
$cache_key = 'woodoo_invoices_' . $partner_id . '_' . $paged;
|
||
$invoices = get_transient( $cache_key );
|
||
|
||
if ( false === $invoices ) {
|
||
$invoices = $api->search_read(
|
||
'account.move',
|
||
$domain,
|
||
[
|
||
'id', 'name', 'invoice_date', 'invoice_date_due',
|
||
'amount_total', 'amount_residual', 'payment_state',
|
||
'currency_id', 'state',
|
||
],
|
||
$per_page,
|
||
$offset,
|
||
'invoice_date desc'
|
||
);
|
||
set_transient( $cache_key, $invoices, 60 );
|
||
}
|
||
|
||
$total = $api->search_count( 'account.move', $domain );
|
||
$num_pages = (int) ceil( $total / $per_page );
|
||
|
||
include WOODOO_DIR . 'templates/myaccount-invoices.php';
|
||
}
|
||
|
||
// ── PDF Proxy ─────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Download an invoice PDF from Odoo and serve it to the logged-in user.
|
||
* ?action=woodoo_invoice_pdf&invoice_id=123&nonce=...
|
||
*/
|
||
public static function ajax_download_pdf(): void {
|
||
$nonce = sanitize_text_field( wp_unslash( $_GET['nonce'] ?? '' ) );
|
||
if ( ! wp_verify_nonce( $nonce, 'woodoo_invoice_pdf' ) ) {
|
||
wp_die( esc_html__( 'Security check failed.', 'woodoo' ) );
|
||
}
|
||
|
||
if ( ! is_user_logged_in() ) {
|
||
wp_die( esc_html__( 'Please log in.', 'woodoo' ) );
|
||
}
|
||
|
||
$invoice_id = absint( $_GET['invoice_id'] ?? 0 );
|
||
if ( ! $invoice_id ) wp_die( 'Invalid invoice ID.' );
|
||
|
||
// Verify the invoice belongs to this user
|
||
$user_id = get_current_user_id();
|
||
$partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true );
|
||
|
||
if ( ! $partner_id ) wp_die( esc_html__( 'Account not linked.', 'woodoo' ) );
|
||
|
||
$api = woodoo_api();
|
||
if ( ! $api ) wp_die( 'API not configured.' );
|
||
|
||
$invoices = $api->search(
|
||
'account.move',
|
||
[ [ 'id', '=', $invoice_id ], [ 'partner_id', '=', $partner_id ] ],
|
||
1
|
||
);
|
||
|
||
if ( empty( $invoices ) ) {
|
||
wp_die( esc_html__( 'Invoice not found.', 'woodoo' ) );
|
||
}
|
||
|
||
// Fetch the PDF report from Odoo
|
||
$odoo_url = rtrim( get_option( 'woodoo_odoo_url', '' ), '/' );
|
||
$pdf_url = $odoo_url . '/report/pdf/account.report_invoice/' . $invoice_id;
|
||
|
||
// Build auth cookie by first doing session authenticate
|
||
$auth_response = wp_remote_post(
|
||
$odoo_url . '/web/session/authenticate',
|
||
[
|
||
'headers' => [ 'Content-Type' => 'application/json' ],
|
||
'body' => wp_json_encode( [
|
||
'jsonrpc' => '2.0',
|
||
'method' => 'call',
|
||
'id' => 1,
|
||
'params' => [
|
||
'db' => get_option( 'woodoo_odoo_db' ),
|
||
'login' => get_option( 'woodoo_odoo_username' ),
|
||
'password' => get_option( 'woodoo_odoo_api_key' ),
|
||
],
|
||
] ),
|
||
'timeout' => 20,
|
||
'sslverify' => apply_filters( 'woodoo_ssl_verify', true ),
|
||
]
|
||
);
|
||
|
||
if ( is_wp_error( $auth_response ) ) {
|
||
wp_die( esc_html__( 'Could not authenticate with Odoo.', 'woodoo' ) );
|
||
}
|
||
|
||
// Extract session cookie
|
||
$raw_headers = wp_remote_retrieve_headers( $auth_response );
|
||
$session_cookie = '';
|
||
if ( isset( $raw_headers['set-cookie'] ) ) {
|
||
$cookie_header = is_array( $raw_headers['set-cookie'] )
|
||
? $raw_headers['set-cookie'][0]
|
||
: $raw_headers['set-cookie'];
|
||
preg_match( '/session_id=([^;]+)/', $cookie_header, $m );
|
||
if ( isset( $m[1] ) ) $session_cookie = 'session_id=' . $m[1];
|
||
}
|
||
|
||
$pdf_response = wp_remote_get(
|
||
$pdf_url,
|
||
[
|
||
'headers' => $session_cookie ? [ 'Cookie' => $session_cookie ] : [],
|
||
'timeout' => 60,
|
||
'sslverify' => apply_filters( 'woodoo_ssl_verify', true ),
|
||
]
|
||
);
|
||
|
||
if ( is_wp_error( $pdf_response ) ) {
|
||
wp_die( esc_html__( 'Could not retrieve invoice PDF.', 'woodoo' ) );
|
||
}
|
||
|
||
$pdf_body = wp_remote_retrieve_body( $pdf_response );
|
||
|
||
header( 'Content-Type: application/pdf' );
|
||
header( 'Content-Disposition: attachment; filename="invoice-' . $invoice_id . '.pdf"' );
|
||
header( 'Content-Length: ' . strlen( $pdf_body ) );
|
||
|
||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||
echo $pdf_body;
|
||
exit;
|
||
}
|
||
|
||
// ── Helper ────────────────────────────────────────────────────────────
|
||
|
||
public static function payment_state_label( string $state ): string {
|
||
$labels = [
|
||
'not_paid' => __( 'Unpaid', 'woodoo' ),
|
||
'partial' => __( 'Partial', 'woodoo' ),
|
||
'in_payment'=> __( 'In Payment', 'woodoo' ),
|
||
'paid' => __( 'Paid', 'woodoo' ),
|
||
'reversed' => __( 'Reversed', 'woodoo' ),
|
||
];
|
||
return $labels[ $state ] ?? ucfirst( $state );
|
||
}
|
||
|
||
public static function payment_state_class( string $state ): string {
|
||
return match ( $state ) {
|
||
'paid' => 'woodoo-badge--green',
|
||
'not_paid' => 'woodoo-badge--red',
|
||
'partial' => 'woodoo-badge--orange',
|
||
'in_payment'=> 'woodoo-badge--blue',
|
||
default => 'woodoo-badge--grey',
|
||
};
|
||
}
|
||
}
|