2026-04-01 13:58:27 +02:00
|
|
|
|
<?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' ] );
|
|
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
// Send invoice by email (AJAX, logged-in users only)
|
|
|
|
|
|
add_action( 'wp_ajax_woodoo_send_invoice_email', [ __CLASS__, 'ajax_send_invoice_email' ] );
|
2026-04-01 13:58:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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' ) {
|
2026-04-01 17:35:15 +02:00
|
|
|
|
$new[ self::ENDPOINT ] = 'Mis Facturas';
|
2026-04-01 13:58:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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 ) {
|
2026-04-01 17:30:10 +02:00
|
|
|
|
echo '<p class="woodoo-notice woodoo-error">' .
|
|
|
|
|
|
'Tu cuenta aún no está vinculada a Odoo. Por favor, contáctanos para activar esta funcionalidad.' .
|
2026-04-01 13:58:27 +02:00
|
|
|
|
'</p>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$api = woodoo_api();
|
|
|
|
|
|
if ( ! $api ) {
|
|
|
|
|
|
echo '<p class="woodoo-notice woodoo-error">' .
|
2026-04-01 17:30:10 +02:00
|
|
|
|
'La integración con Odoo no está configurada. Contacta con soporte.' .
|
2026-04-01 13:58:27 +02:00
|
|
|
|
'</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';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
// ── Send Invoice by Email ─────────────────────────────────────────────
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-01 17:43:39 +02:00
|
|
|
|
* Ask Odoo to resend the invoice email to the customer.
|
|
|
|
|
|
* Uses mail.template.send_mail() with the standard invoice email template.
|
2026-04-01 13:58:27 +02:00
|
|
|
|
*/
|
2026-04-01 17:43:39 +02:00
|
|
|
|
public static function ajax_send_invoice_email(): void {
|
|
|
|
|
|
check_ajax_referer( 'woodoo_invoice_email', 'nonce' );
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
|
|
|
|
|
if ( ! is_user_logged_in() ) {
|
2026-04-01 17:43:39 +02:00
|
|
|
|
wp_send_json_error( 'No autenticado.', 401 );
|
2026-04-01 13:58:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
$invoice_id = absint( $_POST['invoice_id'] ?? 0 );
|
|
|
|
|
|
if ( ! $invoice_id ) {
|
|
|
|
|
|
wp_send_json_error( 'ID de factura no válido.' );
|
|
|
|
|
|
}
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
|
|
|
|
|
$user_id = get_current_user_id();
|
|
|
|
|
|
$partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true );
|
|
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
if ( ! $partner_id ) {
|
|
|
|
|
|
wp_send_json_error( 'Tu cuenta no está vinculada a Odoo.' );
|
|
|
|
|
|
}
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
|
|
|
|
|
$api = woodoo_api();
|
2026-04-01 17:43:39 +02:00
|
|
|
|
if ( ! $api ) {
|
|
|
|
|
|
wp_send_json_error( 'Integración con Odoo no configurada.' );
|
|
|
|
|
|
}
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
// Verify invoice belongs to this customer
|
2026-04-01 13:58:27 +02:00
|
|
|
|
$invoices = $api->search(
|
|
|
|
|
|
'account.move',
|
|
|
|
|
|
[ [ 'id', '=', $invoice_id ], [ 'partner_id', '=', $partner_id ] ],
|
|
|
|
|
|
1
|
|
|
|
|
|
);
|
|
|
|
|
|
if ( empty( $invoices ) ) {
|
2026-04-01 17:43:39 +02:00
|
|
|
|
wp_send_json_error( 'Factura no encontrada.' );
|
2026-04-01 13:58:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
// Find the Odoo invoice email template ID (cached 24h)
|
|
|
|
|
|
$template_id = (int) get_transient( 'woodoo_invoice_tpl_id' );
|
|
|
|
|
|
if ( ! $template_id ) {
|
|
|
|
|
|
$ref = $api->search_read(
|
|
|
|
|
|
'ir.model.data',
|
|
|
|
|
|
[
|
|
|
|
|
|
[ 'module', '=', 'account' ],
|
|
|
|
|
|
[ 'name', '=', 'email_template_edi_invoice' ],
|
|
|
|
|
|
],
|
|
|
|
|
|
[ 'res_id' ],
|
|
|
|
|
|
1
|
|
|
|
|
|
);
|
|
|
|
|
|
$template_id = ! empty( $ref ) ? (int) $ref[0]['res_id'] : 0;
|
|
|
|
|
|
|
|
|
|
|
|
if ( ! $template_id ) {
|
|
|
|
|
|
// Fallback: search templates by model
|
|
|
|
|
|
$tpl = $api->search_read(
|
|
|
|
|
|
'mail.template',
|
|
|
|
|
|
[ [ 'model', '=', 'account.move' ] ],
|
|
|
|
|
|
[ 'id', 'name' ],
|
|
|
|
|
|
1
|
|
|
|
|
|
);
|
|
|
|
|
|
$template_id = ! empty( $tpl ) ? (int) $tpl[0]['id'] : 0;
|
|
|
|
|
|
}
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
if ( $template_id ) {
|
|
|
|
|
|
set_transient( 'woodoo_invoice_tpl_id', $template_id, DAY_IN_SECONDS );
|
|
|
|
|
|
}
|
2026-04-01 17:34:34 +02:00
|
|
|
|
}
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
if ( ! $template_id ) {
|
|
|
|
|
|
wp_send_json_error( 'No se encontró la plantilla de correo de facturas en Odoo.' );
|
2026-04-01 13:58:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
// Call mail.template.send_mail([template_id], invoice_id, force_send=True)
|
|
|
|
|
|
// First arg list [[template_id]] is the recordset selector in execute_kw,
|
|
|
|
|
|
// remaining positional args are passed to the method itself.
|
|
|
|
|
|
$result = $api->execute_kw(
|
|
|
|
|
|
'mail.template',
|
|
|
|
|
|
'send_mail',
|
|
|
|
|
|
[ [ $template_id ], $invoice_id ],
|
|
|
|
|
|
[ 'force_send' => true ]
|
|
|
|
|
|
);
|
2026-04-01 17:30:10 +02:00
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
|
|
|
|
wp_send_json_error( 'Error de Odoo: ' . $result->get_error_message() );
|
2026-04-01 17:30:10 +02:00
|
|
|
|
}
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
2026-04-01 17:43:39 +02:00
|
|
|
|
wp_send_json_success( [
|
|
|
|
|
|
'message' => 'La factura ha sido enviada a tu correo electrónico.',
|
|
|
|
|
|
] );
|
2026-04-01 13:58:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 17:30:10 +02:00
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
2026-04-01 13:58:27 +02:00
|
|
|
|
|
|
|
|
|
|
public static function payment_state_label( string $state ): string {
|
|
|
|
|
|
$labels = [
|
2026-04-01 17:30:10 +02:00
|
|
|
|
'not_paid' => 'Pendiente',
|
|
|
|
|
|
'partial' => 'Parcial',
|
|
|
|
|
|
'in_payment' => 'En cobro',
|
|
|
|
|
|
'paid' => 'Pagado',
|
|
|
|
|
|
'reversed' => 'Anulado',
|
2026-04-01 13:58:27 +02:00
|
|
|
|
];
|
|
|
|
|
|
return $labels[ $state ] ?? ucfirst( $state );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 17:30:10 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Convert an Odoo currency name (e.g. "EUR") to its symbol ("€").
|
|
|
|
|
|
* Falls back to the original string if not in the map.
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function currency_symbol( string $currency_name ): string {
|
|
|
|
|
|
$map = [
|
|
|
|
|
|
'EUR' => '€',
|
|
|
|
|
|
'USD' => '$',
|
|
|
|
|
|
'GBP' => '£',
|
|
|
|
|
|
'JPY' => '¥',
|
|
|
|
|
|
'CHF' => 'Fr',
|
|
|
|
|
|
'MXN' => '$',
|
|
|
|
|
|
'BRL' => 'R$',
|
|
|
|
|
|
'ARS' => '$',
|
|
|
|
|
|
'CLP' => '$',
|
|
|
|
|
|
'COP' => '$',
|
|
|
|
|
|
'PEN' => 'S/',
|
|
|
|
|
|
'DKK' => 'kr',
|
|
|
|
|
|
'SEK' => 'kr',
|
|
|
|
|
|
'NOK' => 'kr',
|
|
|
|
|
|
'PLN' => 'zł',
|
|
|
|
|
|
'CZK' => 'Kč',
|
|
|
|
|
|
'HUF' => 'Ft',
|
|
|
|
|
|
'RON' => 'lei',
|
|
|
|
|
|
'BGN' => 'лв',
|
|
|
|
|
|
'HRK' => 'kn',
|
|
|
|
|
|
'RUB' => '₽',
|
|
|
|
|
|
'TRY' => '₺',
|
|
|
|
|
|
'CNY' => '¥',
|
|
|
|
|
|
'KRW' => '₩',
|
|
|
|
|
|
'INR' => '₹',
|
|
|
|
|
|
'AED' => 'د.إ',
|
|
|
|
|
|
'MAD' => 'MAD',
|
|
|
|
|
|
];
|
|
|
|
|
|
return $map[ strtoupper( $currency_name ) ] ?? $currency_name;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 13:58:27 +02:00
|
|
|
|
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',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|