load_actions(); } /** * Méthode appelée lors de l'activation du plugin. * * Elle délègue au modèle principal la création de la table * `esi_peppol_invoices` et détecte automatiquement les champs TVA. * * @return void */ public static function activate(): void { // Création / mise à jour de la table principale des factures PEPPOL_Main_model::create_table(); // Détection automatique des champs TVA (billing -> shipping) self::auto_detect_vat_field_on_install(); } public function desactivate() { } public static function register_routes() { } public function load_actions() { // Enregistrement des hooks WooCommerce if (class_exists(\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class)) { \ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::register_hooks(); } // Notice globale pour le numéro de TVA \add_action('admin_notices', [self::class, 'maybe_show_vat_notice']); // Ajouter une colonne "Statut Peppol" dans le listing des commandes WooCommerce if (class_exists(\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class)) { \add_filter('manage_edit-shop_order_columns', [\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class, 'add_peppol_status_column'], 20); \add_filter('manage_woocommerce_page_wc-orders_columns', [\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class, 'add_peppol_status_column'], 20); \add_action('manage_shop_order_posts_custom_column', [\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class, 'show_peppol_status_column'], 10, 2); \add_action('manage_woocommerce_page_wc-orders_custom_column', [\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class, 'show_peppol_status_column'], 10, 2); } } /** * Enregistre le menu et les sous-menus dans l'admin WordPress. * * @return void */ public static function add_admin_menu(): void { // Page principale du plugin add_menu_page( __('ESI Peppol', 'esi_peppol'), __('ESI Peppol', 'esi_peppol'), 'manage_options', 'esi-peppol', [self::class, 'render_dashboard_page'], 'dashicons-migrate', 56 ); // Sous-menus (cibles des boutons de la page d'accueil) add_submenu_page( 'esi-peppol', __('Configuration', 'esi_peppol'), __('Configuration', 'esi_peppol'), 'manage_options', 'esi-peppol-settings', [self::class, 'render_settings_page'] ); add_submenu_page( 'esi-peppol', __('Journal', 'esi_peppol'), __('Journal', 'esi_peppol'), 'manage_options', 'esi-peppol-logs', [self::class, 'render_logs_page'] ); } public static function load_filters() { } public function load_shortcodes() { } public function load_frontend_assets() { self::enqueue_admin_assets(); self::enqueue_front_assets(); } public function load_admin_assets() { } /** * Page d'accueil du plugin (dashboard interne ESI Peppol). * * Contient un texte explicatif et deux boutons vers les sous-menus. * * @return void */ public static function render_dashboard_page(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Vous n\'avez pas les permissions nécessaires pour accéder à cette page.', 'esi_peppol')); } $settings_url = admin_url('admin.php?page=esi-peppol-settings'); $logs_url = admin_url('admin.php?page=esi-peppol-logs'); $template = ESI_PEPPOL_DIR . 'templates/admin/dashboard.php'; if (file_exists($template)) { /** @noinspection PhpIncludeInspection */ include $template; } } /** * Page de configuration (stub pour l'instant). * * @return void */ public static function render_settings_page(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Vous n\'avez pas les permissions nécessaires pour accéder à cette page.', 'esi_peppol')); } $notice = null; $error = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = isset($_POST['esi_peppol_action']) ? sanitize_text_field(wp_unslash($_POST['esi_peppol_action'])) : ''; if (!isset($_POST['esi_peppol_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['esi_peppol_nonce'])), 'esi_peppol_save_settings')) { $error = __('Sécurité : nonce invalide.', 'esi_peppol'); } else { $api_key = isset($_POST['esi_peppol_api_key']) ? sanitize_text_field(wp_unslash($_POST['esi_peppol_api_key'])) : ''; $password = isset($_POST['esi_peppol_password']) ? sanitize_text_field(wp_unslash($_POST['esi_peppol_password'])) : ''; $email = isset($_POST['esi_peppol_email']) ? sanitize_email(wp_unslash($_POST['esi_peppol_email'])) : ''; $logo_email_id = isset($_POST['esi_peppol_logo_email_id']) ? absint(wp_unslash($_POST['esi_peppol_logo_email_id'])) : 0; $invoice_number_format = get_option('esi_peppol_invoice_number_format', ''); if ($invoice_number_format === '') { $invoice_number_format = get_option('esi_peppol_vat_number_format', ''); } if (isset($_POST['esi_peppol_invoice_number_format'])) { $invoice_number_format = sanitize_text_field(wp_unslash($_POST['esi_peppol_invoice_number_format'])); } $invoice_number_format = self::normalize_invoice_number_format($invoice_number_format); // Sauvegarde systématique des options update_option('esi_peppol_api_key', $api_key); update_option('esi_peppol_password', $password); update_option('esi_peppol_email', $email); update_option('esi_peppol_logo_email_id', $logo_email_id); update_option('esi_peppol_invoice_number_format', $invoice_number_format); if ($action === 'save') { $notice = __('Paramètres enregistrés avec succès.', 'esi_peppol'); } elseif ($action === 'test') { // Ici on pourrait appeler un endpoint de "ping" de l\'API ESIPeppol. // Pour l\'instant : simple message de succès basé sur la présence des credentials. if ($api_key && $password) { $notice = __('Test de connexion simulé : les identifiants semblent corrects (vérification API réelle à implémenter).', 'esi_peppol'); } else { $error = __('Veuillez renseigner l\'API Key et le Password avant de tester la connexion.', 'esi_peppol'); } } } } $template = ESI_PEPPOL_DIR . 'templates/admin/settings.php'; if (file_exists($template)) { /** @noinspection PhpIncludeInspection */ include $template; } } /** * Page de journal / logs (stub pour l'instant). * * @return void */ public static function render_logs_page(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Vous n\'avez pas les permissions nécessaires pour accéder à cette page.', 'esi_peppol')); } // Vérifier si on affiche la page de détail $detail_id = isset($_GET['detail']) ? (int) $_GET['detail'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ($detail_id > 0) { // Afficher la page de détail $row = PEPPOL_Main_model::get_by_id($detail_id); if (!$row) { wp_die(esc_html__('Aucun enregistrement Peppol trouvé pour cet ID.', 'esi_peppol')); } // Préparer les données pour l'affichage (même logique que ajax_get_log_details) $data_sent = $row->data_sent ?? null; $response_data = $row->response_data ?? null; // Formater les données JSON si possible $data_sent_formatted = ''; if ($data_sent) { if (is_array($data_sent) || is_object($data_sent)) { $data_sent_formatted = wp_json_encode($data_sent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $data_sent_formatted = (string) $data_sent; } } $response_data_formatted = ''; if ($response_data) { if (is_array($response_data) || is_object($response_data)) { $response_data_formatted = wp_json_encode($response_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $response_data_formatted = (string) $response_data; } } // Extraire les informations financières depuis data_sent $invoice_totals = null; $vat_totals = null; $customer_data = null; if ($data_sent && (is_array($data_sent) || is_object($data_sent))) { // Convertir en tableau si c'est un objet $data_array = is_object($data_sent) ? (array) $data_sent : $data_sent; // Extraire invoice_totals if (isset($data_array['invoice_totals']) && is_array($data_array['invoice_totals'])) { $invoice_totals = $data_array['invoice_totals']; } // Extraire vat_totals if (isset($data_array['vat_totals']) && is_array($data_array['vat_totals'])) { $vat_totals = $data_array['vat_totals']; } // Extraire les données client (buyer) if (isset($data_array['parties']['buyer']) && is_array($data_array['parties']['buyer'])) { $customer_data = $data_array['parties']['buyer']; } } // Récupérer les informations de la commande si disponible $order_info = null; $order_totals = null; if (!empty($row->id_order)) { $order = wc_get_order($row->id_order); if ($order) { $order_info = [ 'id' => $order->get_id(), 'number' => $order->get_order_number(), 'status' => $order->get_status(), 'total' => $order->get_total(), 'billing_name' => trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()), 'billing_email' => $order->get_billing_email(), 'billing_phone' => $order->get_billing_phone(), 'billing_company' => $order->get_billing_company(), 'billing_address_1' => $order->get_billing_address_1(), 'billing_address_2' => $order->get_billing_address_2(), 'billing_city' => $order->get_billing_city(), 'billing_postcode' => $order->get_billing_postcode(), 'billing_country' => $order->get_billing_country(), 'billing_vat_number' => $order->get_meta('_billing_vat_number') ?: '', 'edit_link' => get_edit_post_link($row->id_order), ]; // Calculer les totaux depuis la commande si invoice_totals n'est pas disponible if (!$invoice_totals) { $order_totals = [ 'total_amount_excluding_vat' => $order->get_total() - $order->get_total_tax(), 'total_vat_amount' => $order->get_total_tax(), 'total_amount_including_vat' => $order->get_total(), ]; } } } // Extraire le message d'erreur depuis response_data $error_message_to_display = $row->message ?? ''; if (!empty($response_data) && (is_array($response_data) || is_object($response_data))) { // Convertir en tableau si c'est un objet $error_data = is_object($response_data) ? (array) $response_data : $response_data; if (is_array($error_data)) { // Structure avec error.message if (isset($error_data['error']['message'])) { $error_message_to_display = (string) $error_data['error']['message']; } // Structure avec details.validation_error elseif (isset($error_data['details']['validation_error'])) { $error_message_to_display = (string) $error_data['details']['validation_error']; } // Structure avec validation_error directement elseif (isset($error_data['validation_error'])) { $error_message_to_display = (string) $error_data['validation_error']; } // Structure avec message directement elseif (isset($error_data['message'])) { $error_message_to_display = (string) $error_data['message']; } } } // Préparer les données pour le template $log_data = [ 'id' => $row->id, 'id_order' => $row->id_order ?? 0, 'order_info' => $order_info, 'document_id' => $row->document_id ?? '', 'peppol_document_id' => $row->peppol_document_id ?? '', 'status' => $row->status ?? '', 'success' => !empty($row->success), 'message' => $error_message_to_display, 'http_code' => $row->http_code ?? null, 'date_add' => $row->date_add ?? '', 'date_update' => $row->date_update ?? '', 'invoice_totals' => $invoice_totals ?? $order_totals, 'vat_totals' => $vat_totals, 'customer_data' => $customer_data, 'data_sent' => $data_sent_formatted, 'response_data' => $response_data_formatted, ]; $template = ESI_PEPPOL_DIR . 'templates/admin/logs-detail.php'; if (file_exists($template)) { /** @noinspection PhpIncludeInspection */ include $template; } else { wp_die(esc_html__('Template de détail introuvable.', 'esi_peppol')); } } else { // Afficher la liste des logs // Récupération des dernières lignes de la table custom $rows = PEPPOL_Main_model::get_recent(50); $template = ESI_PEPPOL_DIR . 'templates/admin/logs.php'; if (file_exists($template)) { /** @noinspection PhpIncludeInspection */ include $template; } } } public static function enqueue_admin_assets() { $screen = function_exists('get_current_screen') ? get_current_screen() : null; // Ne charger les assets que sur les pages du plugin ou la page des commandes WooCommerce $is_peppol_page = isset($_GET['page']) && strpos((string) $_GET['page'], 'esi-peppol') === 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $is_orders_page = $screen && ( strpos((string) $screen->id, 'shop_order') !== false || strpos((string) $screen->id, 'woocommerce_page_wc-orders') !== false || $screen->id === 'edit-shop_order' ); if (!$is_peppol_page && !$is_orders_page && $screen && strpos((string) $screen->base, 'esi-peppol') === false) { return; } $suffix = defined('SCRIPT_DEBUG') && SCRIPT_DEBUG ? '' : '.min'; // Styles admin généraux wp_enqueue_style( 'esi-peppol-admin', ESI_PEPPOL_URL . 'assets/css/admin.css', [], ESI_PEPPOL_VERSION ); // DataTables (utilisé pour le tableau du journal des échanges) // Utiliser le CDN DataTables pour une meilleure compatibilité avec WordPress wp_enqueue_style( 'esi-peppol-datatables', 'https://cdn.datatables.net/1.13.8/css/jquery.dataTables.min.css', ['esi-peppol-admin'], '1.13.8' ); // S'assurer que jQuery est chargé en premier wp_enqueue_script('jquery'); // Utiliser le CDN DataTables au lieu du fichier local pour éviter les problèmes de compatibilité wp_enqueue_script( 'esi-peppol-datatables', 'https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js', ['jquery'], '1.13.8', true ); // Enqueue wp.media pour la gestion de la médiathèque (logo email) wp_enqueue_media(); // Script admin principal, dépendant de DataTables wp_enqueue_script( 'esi-peppol-admin', ESI_PEPPOL_URL . 'assets/js/admin.js', ['jquery', 'esi-peppol-datatables'], ESI_PEPPOL_VERSION, true ); wp_localize_script( 'esi-peppol-admin', 'esiPeppolAdmin', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('esi_peppol_test_connection'), 'logs_nonce_resend' => wp_create_nonce('esi_peppol_resend_invoice'), 'logs_nonce_status' => wp_create_nonce('esi_peppol_check_invoice_status'), 'logs_nonce_details' => wp_create_nonce('esi_peppol_get_log_details'), 'vat_detect_nonce' => wp_create_nonce('esi_peppol_detect_vat_fields'), 'vat_save_nonce' => wp_create_nonce('esi_peppol_save_vat_field'), 'i18n_missing' => __('Veuillez renseigner l\'API Key et le Password avant de tester la connexion.', 'esi_peppol'), 'i18n_success' => __('Connexion réussie à l\'API ESIPeppol.', 'esi_peppol'), 'i18n_error' => __('La connexion a échoué. Veuillez vérifier vos identifiants.', 'esi_peppol'), 'i18n_network' => __('Erreur de communication avec le serveur WordPress.', 'esi_peppol'), 'i18n_logs_ok_title' => __('OK', 'esi_peppol'), 'i18n_logs_ko_title' => __('Pas OK', 'esi_peppol'), 'i18n_logs_resend_ok' => __('Document renvoyé avec succès à l\'API ESIPeppol.', 'esi_peppol'), 'i18n_logs_resend_ko' => __('L\'envoi du document a échoué.', 'esi_peppol'), 'i18n_logs_status_lbl' => __('Statut actuel du document', 'esi_peppol'), 'i18n_logs_detail_lbl' => __('Détail retour API', 'esi_peppol'), 'i18n_logs_general_info' => __('Informations générales', 'esi_peppol'), 'i18n_logs_totals' => __('Totaux', 'esi_peppol'), 'i18n_logs_vat_details' => __('Détails TVA', 'esi_peppol'), 'i18n_logs_customer_data' => __('Données client', 'esi_peppol'), 'i18n_logs_data_sent' => __('Données envoyées', 'esi_peppol'), 'i18n_logs_response_data' => __('Données de réponse', 'esi_peppol'), 'i18n_logs_error' => __('Erreur lors du chargement des détails.', 'esi_peppol'), ] ); } /** * Handler AJAX pour le test de connexion à l'API ESIPeppol. * * @return void */ public static function ajax_test_connection(): void { if (!check_ajax_referer('esi_peppol_test_connection', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer ce test.', 'esi_peppol'), ], 403 ); } $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $password = isset($_POST['password']) ? sanitize_text_field(wp_unslash($_POST['password'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($api_key === '' || $password === '') { wp_send_json_error( [ 'message' => __('Veuillez renseigner l\'API Key et le Password avant de tester la connexion.', 'esi_peppol'), ], 400 ); } $result = \ESI_PEPPOL\controllers\PEPPOL_peppol_controller::status_with_credentials($api_key, $password); if (!empty($result['success'])) { $company = ''; if (is_array($result['data'] ?? null) && isset($result['data']['company'])) { $company = (string) $result['data']['company']; } $message = $company ? sprintf(__('Connexion réussie. Entreprise: %s', 'esi_peppol'), $company) : __('Connexion réussie à l\'API ESIPeppol.', 'esi_peppol'); wp_send_json_success( [ 'message' => $message, 'http_code' => $result['http_code'] ?? 200, ] ); } $error_message = $result['message'] ?? ''; if ($error_message === '' && is_array($result['data'] ?? null) && isset($result['data']['message'])) { $error_message = (string) $result['data']['message']; } if ($error_message === '') { $error_message = __('La connexion a échoué. Veuillez vérifier vos identifiants.', 'esi_peppol'); } wp_send_json_error( [ 'message' => $error_message, 'http_code' => $result['http_code'] ?? 0, ] ); } /** * Handler AJAX pour renvoyer un document/facture vers l'API ESIPeppol * à partir d'un ID de commande WooCommerce. * * @return void */ public static function ajax_resend_invoice(): void { if (!check_ajax_referer('esi_peppol_resend_invoice', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_woocommerce') && !current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $order_id = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($order_id <= 0) { wp_send_json_error( [ 'message' => __('Identifiant de commande invalide.', 'esi_peppol'), ], 400 ); } $order = wc_get_order($order_id); if (!$order instanceof \WC_Order) { wp_send_json_error( [ 'message' => __('Commande introuvable.', 'esi_peppol'), ], 404 ); } // Vérifier que la clé API est configurée $api_key = get_option('esi_peppol_api_key', ''); if (empty($api_key)) { wp_send_json_error( [ 'message' => __('La clé API n\'est pas configurée. Veuillez configurer la clé API dans les paramètres du plugin.', 'esi_peppol'), ], 400 ); } // Reconstruire le payload et renvoyer le document via l'API ESIPeppol $payload = \ESI_PEPPOL\controllers\PEPPOL_peppol_controller::build_payload_from_order($order); $result = \ESI_PEPPOL\controllers\PEPPOL_peppol_controller::upload_json($payload, $order_id); // Mettre à jour quelques métadonnées sur la commande pour le suivi $order->update_meta_data('_esi_peppol_last_manual_action', 'resend'); $order->update_meta_data('_esi_peppol_last_peppol_status', !empty($result['success']) ? 'success' : 'error'); $order->update_meta_data('_esi_peppol_last_http_code', $result['http_code'] ?? 0); $order->update_meta_data('_esi_peppol_last_message', $result['message'] ?? ''); $order->save(); $detail_message = ''; if (is_array($result['data'] ?? null) && isset($result['data']['message'])) { $detail_message = (string) $result['data']['message']; } if ($detail_message === '' && isset($result['message'])) { $detail_message = (string) $result['message']; } $payload_response = [ 'success' => !empty($result['success']), 'message' => !empty($result['success']) ? __('Document renvoyé avec succès à l\'API ESIPeppol.', 'esi_peppol') : __('L\'envoi du document a échoué.', 'esi_peppol'), 'detail' => $detail_message, 'http_code' => $result['http_code'] ?? 0, ]; if (!empty($result['success'])) { wp_send_json_success($payload_response); } wp_send_json_error($payload_response, $result['http_code'] ?? 400); } /** * Handler AJAX pour consulter le statut courant d'un document Peppol * à partir de l'ID de commande (lecture depuis la table custom). * * @return void */ public static function ajax_check_invoice_status(): void { if (!check_ajax_referer('esi_peppol_check_invoice_status', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_woocommerce') && !current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $order_id = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($order_id <= 0) { wp_send_json_error( [ 'message' => __('Identifiant de commande invalide.', 'esi_peppol'), ], 400 ); } $row = \ESI_PEPPOL\models\PEPPOL_Main_model::get_by_order_id($order_id); if (!$row) { wp_send_json_error( [ 'message' => __('Aucun enregistrement Peppol trouvé pour cette commande.', 'esi_peppol'), ], 404 ); } $detail_message = ''; if (isset($row->response_data)) { $data = $row->response_data; if (is_array($data) && isset($data['message'])) { $detail_message = (string) $data['message']; } elseif (is_string($data) && $data !== '') { $detail_message = $data; } } if ($detail_message === '' && isset($row->message)) { $detail_message = (string) $row->message; } $payload_response = [ 'success' => !empty($row->success), 'status' => isset($row->status) ? (string) $row->status : '', 'message' => sprintf( /* translators: %s: statut Peppol actuel */ __('Statut actuel du document : %s', 'esi_peppol'), isset($row->status) ? (string) $row->status : __('inconnu', 'esi_peppol') ), 'detail' => $detail_message, 'http_code' => isset($row->http_code) ? (int) $row->http_code : 0, ]; if (!empty($row->success)) { wp_send_json_success($payload_response); } // On renvoie tout de même une réponse structurée même en cas d'échec métier wp_send_json_error($payload_response, 200); } /** * Détecte automatiquement les champs TVA lors de l'installation. * Utilise la fonction helper PEPPOL_Woo_Helper::esi_get_vat_meta_keys(). * * @return string|null La clé du champ TVA détecté, ou null si aucun trouvé */ public static function auto_detect_vat_field_on_install(): ?string { // Ne pas détecter si un champ est déjà sauvegardé $existing_field = get_option('esi_peppol_vat_field_key', ''); if ($existing_field !== '') { return $existing_field; } // Vérifier si WooCommerce est disponible if (!function_exists('wc_get_order')) { // Marquer que la détection a été tentée (même si WooCommerce n'est pas disponible) update_option('esi_peppol_vat_field_auto_detected', true); return null; } $vat_keys = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_vat_meta_keys(50); // Si aucun champ trouvé, marquer que la détection a été effectuée et retourner null if (empty($vat_keys)) { update_option('esi_peppol_vat_field_auto_detected', true); return null; } // Prendre la première clé trouvée $vat_field_key = reset($vat_keys); // Sauvegarder le champ détecté update_option('esi_peppol_vat_field_key', $vat_field_key); // Marquer que la détection a été effectuée à l'installation update_option('esi_peppol_vat_field_auto_detected', true); return $vat_field_key; } /** * Handler AJAX pour détecter les champs TVA dans les commandes WooCommerce. * * @return void */ public static function ajax_detect_vat_fields(): void { if (!check_ajax_referer('esi_peppol_detect_vat_fields', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $vat_keys = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_vat_meta_keys(); $vat_fields_found = array_map(function ($key) { return [ 'key' => $key, 'count' => 0, 'sample_value' => null, ]; }, $vat_keys); wp_send_json_success([ 'fields' => array_values($vat_fields_found), 'count' => count($vat_fields_found), ]); } /** * Handler AJAX pour sauvegarder le champ TVA sélectionné. * * @return void */ public static function ajax_save_vat_field(): void { if (!check_ajax_referer('esi_peppol_save_vat_field', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $vat_field_key = isset($_POST['vat_field_key']) ? sanitize_text_field(wp_unslash($_POST['vat_field_key'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($vat_field_key === '') { wp_send_json_error( [ 'message' => __('Clé de champ TVA invalide.', 'esi_peppol'), ], 400 ); } // Sauvegarder le champ sélectionné update_option('esi_peppol_vat_field_key', $vat_field_key); wp_send_json_success([ 'message' => __('Champ TVA enregistré avec succès.', 'esi_peppol'), 'vat_field_key' => $vat_field_key, ]); } /** * Handler AJAX pour récupérer les détails complets d'un log Peppol * à partir de l'ID du log. * * @return void */ public static function ajax_get_log_details(): void { if (!check_ajax_referer('esi_peppol_get_log_details', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_woocommerce') && !current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $log_id = isset($_POST['log_id']) ? (int) $_POST['log_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($log_id <= 0) { wp_send_json_error( [ 'message' => __('Identifiant de log invalide.', 'esi_peppol'), ], 400 ); } $row = \ESI_PEPPOL\models\PEPPOL_Main_model::get_by_id($log_id); if (!$row) { wp_send_json_error( [ 'message' => __('Aucun enregistrement Peppol trouvé pour cet ID.', 'esi_peppol'), ], 404 ); } // Préparer les données pour l'affichage $data_sent = $row->data_sent ?? null; $response_data = $row->response_data ?? null; // Formater les données JSON si possible $data_sent_formatted = ''; if ($data_sent) { if (is_array($data_sent) || is_object($data_sent)) { $data_sent_formatted = wp_json_encode($data_sent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $data_sent_formatted = (string) $data_sent; } } $response_data_formatted = ''; if ($response_data) { if (is_array($response_data) || is_object($response_data)) { $response_data_formatted = wp_json_encode($response_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $response_data_formatted = (string) $response_data; } } // Extraire les informations financières depuis data_sent $invoice_totals = null; $vat_totals = null; $customer_data = null; if ($data_sent && (is_array($data_sent) || is_object($data_sent))) { // Convertir en tableau si c'est un objet $data_array = is_object($data_sent) ? (array) $data_sent : $data_sent; // Extraire invoice_totals if (isset($data_array['invoice_totals']) && is_array($data_array['invoice_totals'])) { $invoice_totals = $data_array['invoice_totals']; } // Extraire vat_totals if (isset($data_array['vat_totals']) && is_array($data_array['vat_totals'])) { $vat_totals = $data_array['vat_totals']; } // Extraire les données client (buyer) if (isset($data_array['parties']['buyer']) && is_array($data_array['parties']['buyer'])) { $customer_data = $data_array['parties']['buyer']; } } // Récupérer les informations de la commande si disponible $order_info = null; $order_totals = null; if (!empty($row->id_order)) { $order = wc_get_order($row->id_order); if ($order) { $order_info = [ 'id' => $order->get_id(), 'number' => $order->get_order_number(), 'status' => $order->get_status(), 'total' => $order->get_total(), 'billing_name' => trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()), 'billing_email' => $order->get_billing_email(), 'billing_phone' => $order->get_billing_phone(), 'billing_company' => $order->get_billing_company(), 'billing_address_1' => $order->get_billing_address_1(), 'billing_address_2' => $order->get_billing_address_2(), 'billing_city' => $order->get_billing_city(), 'billing_postcode' => $order->get_billing_postcode(), 'billing_country' => $order->get_billing_country(), 'billing_vat_number' => $order->get_meta('_billing_vat_number') ?: '', 'edit_link' => get_edit_post_link($row->id_order), ]; // Calculer les totaux depuis la commande si invoice_totals n'est pas disponible if (!$invoice_totals) { $order_totals = [ 'total_amount_excluding_vat' => $order->get_total() - $order->get_total_tax(), 'total_vat_amount' => $order->get_total_tax(), 'total_amount_including_vat' => $order->get_total(), ]; } } } // Extraire le message d'erreur depuis response_data de la même manière que le template email // pour garantir la cohérence entre la modal et l'email $error_message_to_display = $row->message ?? ''; if (!empty($response_data) && (is_array($response_data) || is_object($response_data))) { // Convertir en tableau si c'est un objet $error_data = is_object($response_data) ? (array) $response_data : $response_data; if (is_array($error_data)) { // Structure avec error.message if (isset($error_data['error']['message'])) { $error_message_to_display = (string) $error_data['error']['message']; } // Structure avec details.validation_error elseif (isset($error_data['details']['validation_error'])) { $error_message_to_display = (string) $error_data['details']['validation_error']; } // Structure avec validation_error directement elseif (isset($error_data['validation_error'])) { $error_message_to_display = (string) $error_data['validation_error']; } // Structure avec message directement elseif (isset($error_data['message'])) { $error_message_to_display = (string) $error_data['message']; } } } $payload_response = [ 'id' => $row->id, 'id_order' => $row->id_order ?? 0, 'order_info' => $order_info, 'document_id' => $row->document_id ?? '', 'peppol_document_id' => $row->peppol_document_id ?? '', 'status' => $row->status ?? '', 'success' => !empty($row->success), 'message' => $error_message_to_display, 'http_code' => $row->http_code ?? null, 'date_add' => $row->date_add ?? '', 'date_update' => $row->date_update ?? '', 'invoice_totals' => $invoice_totals ?? $order_totals, 'vat_totals' => $vat_totals, 'customer_data' => $customer_data, 'data_sent' => $data_sent_formatted, 'response_data' => $response_data_formatted, ]; wp_send_json_success($payload_response); } public static function enqueue_front_assets() { } /** * Ajoute un champ "Numéro de TVA de la boutique" dans les réglages * WooCommerce > Général, juste après le code postal du magasin. * * @param array $settings * @return array */ public static function filter_woocommerce_general_settings(array $settings): array { $new_settings = []; foreach ($settings as $setting) { $new_settings[] = $setting; if (isset($setting['id']) && $setting['id'] === 'woocommerce_store_postcode') { $new_settings[] = [ 'title' => __('Numéro de TVA de la boutique', 'esi_peppol'), 'desc' => __('Utilisé pour les factures Peppol (champ vendeur).', 'esi_peppol'), 'id' => 'woocommerce_store_vat_number', 'type' => 'text', 'default' => '', 'desc_tip' => true, ]; } } return $new_settings; } /** * Retourne un message d'avertissement à propos du numéro de TVA * de la boutique si celui-ci n'est pas encore renseigné dans * les réglages WooCommerce, ou si aucun champ TVA client n'a été détecté. * * - Message d'invitation initial : aucune configuration ESI Peppol * n'a encore été saisie. * - Message de rappel : les identifiants ESI Peppol sont déjà * enregistrés mais la TVA reste vide. * - Message si aucun champ TVA client détecté : proposer la détection dans settings. * * @return string|null */ /** * Récupère toutes les notices TVA à afficher * * @return array Tableau de notices, chaque notice contient 'message' et 'type' (info, warning, etc.) */ public static function get_vat_notice_message(): array { $notices = []; // Vérifier d'abord si un champ TVA client a été détecté $vat_field_key = (string) get_option('esi_peppol_vat_field_key', ''); $auto_detected = (bool) get_option('esi_peppol_vat_field_auto_detected', false); // Si aucun champ TVA détecté, proposer la détection dans settings if ($vat_field_key === '' && $auto_detected) { $settings_url = admin_url('admin.php?page=esi-peppol-settings'); $notices[] = [ 'message' => sprintf( /* translators: %s: URL vers la page de réglages */ __('Aucun champ TVA client n\'a été détecté automatiquement. Vous pouvez utiliser l\'outil de détection dans ESI Peppol > Configuration pour rechercher manuellement les champs TVA dans vos commandes.', 'esi_peppol'), esc_url($settings_url) ), 'type' => 'warning' ]; } $vat_number = (string) get_option('woocommerce_store_vat_number', ''); // Si le numéro de TVA est renseigné, on ne vérifie pas les autres notices liées au TVA if ($vat_number === '') { $api_key = (string) get_option('esi_peppol_api_key', ''); $password = (string) get_option('esi_peppol_password', ''); if ($api_key === '' && $password === '') { // Invitation initiale $notices[] = [ 'message' => __( 'Pour finaliser la mise en place de Peppol, pensez à renseigner le numéro de TVA de la boutique dans WooCommerce > Réglages > Général.', 'esi_peppol' ), 'type' => 'info' ]; } else { // Rappel après configuration du connecteur $notices[] = [ 'message' => __( 'Votre connecteur ESI Peppol est configuré, mais le numéro de TVA de la boutique n\'est pas encore renseigné dans WooCommerce > Réglages > Général. Sans ce numéro, les factures Peppol risquent d\'être incomplètes.', 'esi_peppol' ), 'type' => 'warning' ]; } } return $notices; } /** * Affiche un message d'avertissement dans l'admin WordPress * si le numéro de TVA de la boutique n'est pas renseigné ou * si aucun champ TVA client n'a été détecté. * * Hooké sur admin_notices. * * @return void */ public static function maybe_show_vat_notice(): void { if (!\current_user_can('manage_options')) { return; } $vat_notices = self::get_vat_notice_message(); if (empty($vat_notices)) { return; } foreach ($vat_notices as $notice) { $notice_class = 'notice-' . $notice['type']; echo '
' . \wp_kses_post($notice['message']) . '