Firs commit
This commit is contained in:
parent
9c7064db92
commit
86e8d49065
160
ESI_peppol.php
Normal file
160
ESI_peppol.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: ESI_peppol
|
||||
* Description: Woocommerce peppol gateway
|
||||
* Version: 0.1.0
|
||||
* Author: Votre Nom
|
||||
* Text Domain: esi_peppol
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
define('ESI_PEPPOL_VERSION', '0.1.0');
|
||||
define('ESI_PEPPOL_DIR', plugin_dir_path(__FILE__));
|
||||
define('ESI_PEPPOL_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use ESI_PEPPOL\controllers\PEPPOL_Plugin;
|
||||
|
||||
// Autoloader simple pour les classes PEPPOL (contrôleurs, modèles...) et celles avec namespace
|
||||
spl_autoload_register(function ($class) {
|
||||
|
||||
if (strpos($class, 'ESI_PEPPOL\\') === 0 || strpos($class, 'PEPPOL_') === 0) {
|
||||
|
||||
// Gestion des classes avec namespace ESI_PEPPOL
|
||||
if (strpos($class, 'ESI_PEPPOL') === 0) {
|
||||
$base_dir = __DIR__ . '/app/';
|
||||
|
||||
$relative_class = substr($class, strlen('ESI_PEPPOL\\'));
|
||||
|
||||
// Mapping simple pour les classes PEPPOL_
|
||||
if (strpos($relative_class, 'controllers\\PEPPOL_') === 0) {
|
||||
$class_name = substr($relative_class, strlen('controllers\\PEPPOL_'));
|
||||
// Pour PEPPOL_Event_Controller -> Event_Controller.php
|
||||
$file = $base_dir . 'controllers/' . $class_name . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($relative_class, 'views\\PEPPOL_') === 0) {
|
||||
$class_name = substr($relative_class, strlen('views\\PEPPOL_'));
|
||||
$file = $base_dir . 'views/' . $class_name . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($relative_class, 'models\\PEPPOL_') === 0) {
|
||||
$class_name = substr($relative_class, strlen('models\\PEPPOL_'));
|
||||
$file = $base_dir . 'models/' . $class_name . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($relative_class, 'helpers\\PEPPOL_') === 0) {
|
||||
$class_name = substr($relative_class, strlen('helpers\\PEPPOL_'));
|
||||
$file = $base_dir . 'helpers/' . $class_name . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Mapping explicite pour les classes PEPPOL_ dont le nom de fichier
|
||||
// ne suit pas strictement le schéma "NomDeClasse.php".
|
||||
switch ($relative_class) {
|
||||
case 'controllers\\PEPPOL_Plugin':
|
||||
$file = $base_dir . 'controllers/Plugin.php';
|
||||
break;
|
||||
case 'controllers\\PEPPOL_Woocommerce_controller':
|
||||
$file = $base_dir . 'controllers/Woocommerce_controller.php';
|
||||
break;
|
||||
case 'controllers\\PEPPOL_peppol_controller':
|
||||
$file = $base_dir . 'controllers/Peppol_controller.php';
|
||||
break;
|
||||
case 'models\\PEPPOL_Main_model':
|
||||
$file = $base_dir . 'models/Main_model.php';
|
||||
break;
|
||||
|
||||
case 'helpers\\PEPPOL_Woo_Helper':
|
||||
// Helper WooCommerce : fichier réel situé dans "app/helper/woo_helper.php"
|
||||
// Attention : sur les systèmes Linux, la casse et le nom du dossier/import doivent correspondre.
|
||||
$file = $base_dir . 'helper/woo_helper.php';
|
||||
break;
|
||||
default:
|
||||
// Fallback : essayer le mapping standard
|
||||
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||
break;
|
||||
}
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hook d'activation du plugin : délègue à PEPPOL_Plugin::activate()
|
||||
register_activation_hook(
|
||||
__FILE__,
|
||||
[PEPPOL_Plugin::class, 'activate']
|
||||
);
|
||||
|
||||
// Menu et pages d'administration du plugin
|
||||
add_action(
|
||||
'admin_menu',
|
||||
[PEPPOL_Plugin::class, 'add_admin_menu']
|
||||
);
|
||||
|
||||
// Assets d'administration
|
||||
add_action(
|
||||
'admin_enqueue_scripts',
|
||||
[PEPPOL_Plugin::class, 'enqueue_admin_assets']
|
||||
);
|
||||
|
||||
// AJAX admin pour test de connexion API
|
||||
add_action(
|
||||
'wp_ajax_esi_peppol_test_connection',
|
||||
[PEPPOL_Plugin::class, 'ajax_test_connection']
|
||||
);
|
||||
|
||||
// AJAX admin pour renvoyer une facture Peppol depuis la page de logs
|
||||
add_action(
|
||||
'wp_ajax_esi_peppol_resend_invoice',
|
||||
[PEPPOL_Plugin::class, 'ajax_resend_invoice']
|
||||
);
|
||||
|
||||
// AJAX admin pour consulter le statut d'une facture Peppol depuis la page de logs
|
||||
add_action(
|
||||
'wp_ajax_esi_peppol_check_invoice_status',
|
||||
[PEPPOL_Plugin::class, 'ajax_check_invoice_status']
|
||||
);
|
||||
|
||||
// Champ TVA de la boutique dans les réglages WooCommerce > Général.
|
||||
add_filter(
|
||||
'woocommerce_general_settings',
|
||||
[PEPPOL_Plugin::class, 'filter_woocommerce_general_settings']
|
||||
);
|
||||
|
||||
// Initialisation générale du plugin (hooks WooCommerce, etc.)
|
||||
add_action(
|
||||
'plugins_loaded',
|
||||
static function () {
|
||||
$plugin = new PEPPOL_Plugin();
|
||||
$plugin->init();
|
||||
},
|
||||
20
|
||||
);
|
||||
1
app/config.php
Normal file
1
app/config.php
Normal file
@ -0,0 +1 @@
|
||||
<?php
|
||||
892
app/controllers/Peppol_controller.php
Normal file
892
app/controllers/Peppol_controller.php
Normal file
@ -0,0 +1,892 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_PEPPOL\controllers;
|
||||
|
||||
use ESI_PEPPOL\models\PEPPOL_Main_model;
|
||||
use ESI_PEPPOL\helpers\PEPPOL_Woo_Helper;
|
||||
|
||||
class PEPPOL_peppol_controller {
|
||||
|
||||
/**
|
||||
* URL de base de l'API ESIPeppol.
|
||||
*/
|
||||
private const API_BASE_URL = 'https://demo.esi-peppol.be/api';
|
||||
|
||||
/**
|
||||
* Retourne l'URL de base de l'API (filtrable).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_base_url(): string {
|
||||
/**
|
||||
* Filtre pour surcharger l'URL de base de l'API ESIPeppol.
|
||||
*
|
||||
* @param string $base_url URL de base actuelle.
|
||||
*/
|
||||
return (string) \apply_filters('esi_peppol_api_base_url', self::API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les identifiants API stockés dans les options WordPress.
|
||||
*
|
||||
* @return array{api_key:string,password:string}
|
||||
*/
|
||||
protected static function get_credentials(): array {
|
||||
$api_key = (string) \get_option('esi_peppol_api_key', '');
|
||||
$password = (string) \get_option('esi_peppol_password', '');
|
||||
|
||||
return [
|
||||
'api_key' => $api_key,
|
||||
'password' => $password,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit les en-têtes HTTP pour les appels authentifiés.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
protected static function get_auth_headers(): array {
|
||||
$creds = self::get_credentials();
|
||||
|
||||
return [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
'X-API-KEY' => $creds['api_key'],
|
||||
'X-PASSWORD' => $creds['password'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Appel générique à l'API ESIPeppol.
|
||||
*
|
||||
* @param string $method Méthode HTTP (GET, POST, ...).
|
||||
* @param string $path Chemin de l'endpoint (ex. '/health').
|
||||
* @param array $args Arguments supplémentaires (body, headers...).
|
||||
* @param bool $with_auth Indique si l'appel doit être authentifié.
|
||||
*
|
||||
* @return array{
|
||||
* success:bool,
|
||||
* http_code:int,
|
||||
* message:string,
|
||||
* data:mixed
|
||||
* }
|
||||
*/
|
||||
protected static function request(string $method, string $path, array $args = [], bool $with_auth = true): array {
|
||||
$base_url = rtrim(self::get_base_url(), '/');
|
||||
$path = '/' . ltrim($path, '/');
|
||||
$url = $base_url . $path;
|
||||
|
||||
$headers = [
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
|
||||
if ($with_auth) {
|
||||
$headers = array_merge($headers, self::get_auth_headers());
|
||||
}
|
||||
|
||||
if (isset($args['headers']) && is_array($args['headers'])) {
|
||||
$headers = array_merge($headers, $args['headers']);
|
||||
}
|
||||
|
||||
$wp_args = [
|
||||
'method' => $method,
|
||||
'timeout' => 20,
|
||||
'headers' => $headers,
|
||||
];
|
||||
|
||||
if (isset($args['body'])) {
|
||||
$body = $args['body'];
|
||||
|
||||
if (!is_string($body)) {
|
||||
$body = \wp_json_encode($body);
|
||||
}
|
||||
|
||||
$wp_args['body'] = $body;
|
||||
}
|
||||
|
||||
$response = \wp_remote_request($url, $wp_args);
|
||||
|
||||
if (\is_wp_error($response)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'http_code' => 0,
|
||||
'message' => $response->get_error_message(),
|
||||
'data' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$code = (int) \wp_remote_retrieve_response_code($response);
|
||||
$body = \wp_remote_retrieve_body($response);
|
||||
|
||||
$data = null;
|
||||
if ($body !== '') {
|
||||
$decoded = \json_decode($body, true);
|
||||
$data = \json_last_error() === \JSON_ERROR_NONE ? $decoded : $body;
|
||||
}
|
||||
|
||||
$success = $code >= 200 && $code < 300;
|
||||
|
||||
$message = '';
|
||||
if (!$success) {
|
||||
if (is_array($data) && isset($data['message'])) {
|
||||
$message = (string) $data['message'];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $success,
|
||||
'http_code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelle l'endpoint public /health (sans authentification).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function health_check(): array {
|
||||
return self::request('GET', '/health', [], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelle l'endpoint /status (avec authentification) pour vérifier
|
||||
* la validité des credentials et l'état du quota.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function status(): array {
|
||||
return self::request('GET', '/status', [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variante de /status qui utilise explicitement les identifiants fournis,
|
||||
* sans passer par les options WordPress (utile pour les tests de connexion).
|
||||
*
|
||||
* @param string $api_key
|
||||
* @param string $password
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function status_with_credentials(string $api_key, string $password): array {
|
||||
$headers = [
|
||||
'X-API-KEY' => $api_key,
|
||||
'X-PASSWORD' => $password,
|
||||
];
|
||||
|
||||
return self::request('GET', '/status', ['headers' => $headers], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un JSON déjà construit vers /upload-json.
|
||||
*
|
||||
* Si un $order_id est fourni, le résultat est enregistré dans la table custom.
|
||||
*
|
||||
* @param array $payload Données JSON structurées prêtes pour l'API.
|
||||
* @param int|null $order_id ID de commande WooCommerce lié (optionnel).
|
||||
*
|
||||
* @return array Réponse normalisée (success, http_code, message, data).
|
||||
*/
|
||||
public static function upload_json(array $payload, ?int $order_id = null): array {
|
||||
$result = self::request('POST', '/upload-json', ['body' => $payload], true);
|
||||
|
||||
if ($order_id !== null) {
|
||||
$data = is_array($result['data'] ?? null) ? $result['data'] : [];
|
||||
|
||||
// Extraire document_id et peppol_document_id depuis la réponse API
|
||||
$document_id = isset($data['document_id']) ? (string) $data['document_id'] : '';
|
||||
$peppol_document_id = isset($data['peppol_document_id']) ? (string) $data['peppol_document_id'] : '';
|
||||
$status = isset($data['status']) ? (string) $data['status'] : ($result['success'] ? 'created' : 'error');
|
||||
|
||||
$message = $result['message'];
|
||||
if ($message === '' && isset($data['message'])) {
|
||||
$message = (string) $data['message'];
|
||||
}
|
||||
|
||||
// Log technique optionnel pour vérifier le payload réellement envoyé
|
||||
if (class_exists(PEPPOL_Plugin::class)) {
|
||||
PEPPOL_Plugin::write_debug_file(
|
||||
[
|
||||
'event' => 'upload_json',
|
||||
'order_id' => $order_id,
|
||||
'payload_sent' => $payload,
|
||||
'api_result' => $result,
|
||||
'document_id' => $document_id,
|
||||
'peppol_document_id' => $peppol_document_id,
|
||||
],
|
||||
'INFO'
|
||||
);
|
||||
}
|
||||
|
||||
// Enregistrer ou mettre à jour dans la base de données
|
||||
PEPPOL_Main_model::save_for_order(
|
||||
$order_id,
|
||||
$data,
|
||||
$payload,
|
||||
$document_id,
|
||||
$peppol_document_id,
|
||||
$status,
|
||||
(bool) $result['success'],
|
||||
$message,
|
||||
(int) $result['http_code']
|
||||
);
|
||||
|
||||
// Envoyer un email en cas d'erreur API
|
||||
if (!$result['success'] && class_exists(PEPPOL_Plugin::class)) {
|
||||
PEPPOL_Plugin::send_api_error_email($order_id, $result, $payload);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute au tableau des totaux de TVA les montants d'une ligne,
|
||||
* en gérant les cas avec plusieurs taux sur la même ligne.
|
||||
*
|
||||
* @param array<string,array<string,mixed>> $vat_totals_by_rate_code
|
||||
* @param float $taxable_amount Montant HT de la ligne (tous taux confondus).
|
||||
* @param float $vat_amount Montant TVA total de la ligne.
|
||||
* @param array<int,array<string,mixed>> $tax_details Détails TVA par taux (helper my_get_item_tax_details).
|
||||
* @param float $fallback_vat_rate Taux utilisé si aucun détail exploitable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function accumulate_vat_totals_for_line(
|
||||
array &$vat_totals_by_rate_code,
|
||||
float $taxable_amount,
|
||||
float $vat_amount,
|
||||
array $tax_details,
|
||||
float $fallback_vat_rate
|
||||
): void {
|
||||
// Si pas de détails ou TVA totale nulle, on retombe sur l'ancien comportement (un seul taux).
|
||||
$positive_taxes = array_filter(
|
||||
$tax_details,
|
||||
static function ($detail): bool {
|
||||
return isset($detail['amount'], $detail['percent'])
|
||||
&& (float) $detail['amount'] > 0.0
|
||||
&& (float) $detail['percent'] > 0.0;
|
||||
}
|
||||
);
|
||||
|
||||
if (empty($positive_taxes) || $vat_amount <= 0.0 || $taxable_amount <= 0.0) {
|
||||
$key = (string) $fallback_vat_rate;
|
||||
|
||||
if (!isset($vat_totals_by_rate_code[$key])) {
|
||||
$vat_totals_by_rate_code[$key] = [
|
||||
'vat_rate' => $fallback_vat_rate,
|
||||
// La catégorie sera recalculée après accumulation des montants
|
||||
'vat_category_code' => 'S',
|
||||
'taxable_amount' => 0.0,
|
||||
'vat_amount' => 0.0,
|
||||
'tax_scheme_id' => 'VAT',
|
||||
'tax_scheme_name' => 'Value Added Tax',
|
||||
];
|
||||
}
|
||||
|
||||
$vat_totals_by_rate_code[$key]['taxable_amount'] += $taxable_amount;
|
||||
$vat_totals_by_rate_code[$key]['vat_amount'] += $vat_amount;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Plusieurs taux positifs sur la même ligne : on répartit le HT au prorata des montants de TVA.
|
||||
$total_positive_vat = 0.0;
|
||||
foreach ($positive_taxes as $detail) {
|
||||
$total_positive_vat += (float) $detail['amount'];
|
||||
}
|
||||
|
||||
if ($total_positive_vat <= 0.0) {
|
||||
// Sécurité : repli sur le comportement ancien.
|
||||
$key = (string) $fallback_vat_rate;
|
||||
|
||||
if (!isset($vat_totals_by_rate_code[$key])) {
|
||||
$vat_totals_by_rate_code[$key] = [
|
||||
'vat_rate' => $fallback_vat_rate,
|
||||
'vat_category_code' => 'S',
|
||||
'taxable_amount' => 0.0,
|
||||
'vat_amount' => 0.0,
|
||||
'tax_scheme_id' => 'VAT',
|
||||
'tax_scheme_name' => 'Value Added Tax',
|
||||
];
|
||||
}
|
||||
|
||||
$vat_totals_by_rate_code[$key]['taxable_amount'] += $taxable_amount;
|
||||
$vat_totals_by_rate_code[$key]['vat_amount'] += $vat_amount;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$allocated_taxable = 0.0;
|
||||
$positive_count = count($positive_taxes);
|
||||
$index = 0;
|
||||
|
||||
foreach ($positive_taxes as $detail) {
|
||||
++$index;
|
||||
|
||||
$rate = (float) $detail['percent'];
|
||||
$tax_part = (float) $detail['amount'];
|
||||
$rate_key = (string) $rate;
|
||||
|
||||
// Répartition du HT au prorata de la TVA de ce taux.
|
||||
if ($index === $positive_count) {
|
||||
// Dernier taux : on prend le reste pour éviter les erreurs d'arrondi.
|
||||
$taxable_share = $taxable_amount - $allocated_taxable;
|
||||
} else {
|
||||
$taxable_share = $taxable_amount * ($tax_part / $total_positive_vat);
|
||||
$allocated_taxable += $taxable_share;
|
||||
}
|
||||
|
||||
if (!isset($vat_totals_by_rate_code[$rate_key])) {
|
||||
$vat_totals_by_rate_code[$rate_key] = [
|
||||
'vat_rate' => $rate,
|
||||
'vat_category_code' => 'S',
|
||||
'taxable_amount' => 0.0,
|
||||
'vat_amount' => 0.0,
|
||||
'tax_scheme_id' => 'VAT',
|
||||
'tax_scheme_name' => 'Value Added Tax',
|
||||
];
|
||||
}
|
||||
|
||||
$vat_totals_by_rate_code[$rate_key]['taxable_amount'] += $taxable_share;
|
||||
$vat_totals_by_rate_code[$rate_key]['vat_amount'] += $tax_part;
|
||||
}
|
||||
|
||||
// S'il reste une partie du HT non allouée (par ex. combiné à un taux 0%),
|
||||
// on l'ajoute dans un bucket 0% (exempt).
|
||||
$remaining_taxable = $taxable_amount - $allocated_taxable;
|
||||
if ($remaining_taxable > 0.0001) {
|
||||
$key_zero = '0';
|
||||
|
||||
if (!isset($vat_totals_by_rate_code[$key_zero])) {
|
||||
$vat_totals_by_rate_code[$key_zero] = [
|
||||
'vat_rate' => 0.0,
|
||||
'vat_category_code' => 'E',
|
||||
'taxable_amount' => 0.0,
|
||||
'vat_amount' => 0.0,
|
||||
'tax_scheme_id' => 'VAT',
|
||||
'tax_scheme_name' => 'Value Added Tax',
|
||||
];
|
||||
}
|
||||
|
||||
$vat_totals_by_rate_code[$key_zero]['taxable_amount'] += $remaining_taxable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le payload JSON attendu par /upload-json à partir
|
||||
* d'une commande WooCommerce.
|
||||
*
|
||||
* @param \WC_Order $order
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function build_payload_from_order(\WC_Order $order): array {
|
||||
$order_id = $order->get_id();
|
||||
$order_number = $order->get_order_number();
|
||||
$currency_code = $order->get_currency();
|
||||
|
||||
$invoice_date = $order->get_date_created()
|
||||
? $order->get_date_created()->date('Y-m-d')
|
||||
: \gmdate('Y-m-d');
|
||||
|
||||
// Par défaut, échéance à +30 jours (filtrable)
|
||||
$due_date = \apply_filters(
|
||||
'esi_peppol_invoice_due_date',
|
||||
(new \DateTimeImmutable($invoice_date))
|
||||
->modify('+30 days')
|
||||
->format('Y-m-d'),
|
||||
$order
|
||||
);
|
||||
|
||||
$external_reference = \apply_filters(
|
||||
'esi_peppol_external_reference',
|
||||
'WC-' . $order_number,
|
||||
$order
|
||||
);
|
||||
|
||||
$invoice_notes = \apply_filters(
|
||||
'esi_peppol_invoice_notes',
|
||||
sprintf(
|
||||
/* translators: %s: order number */
|
||||
__('Facture générée depuis la commande WooCommerce %s', 'esi_peppol'),
|
||||
$order_number
|
||||
),
|
||||
$order
|
||||
);
|
||||
|
||||
$payment_terms = \apply_filters(
|
||||
'esi_peppol_payment_terms',
|
||||
__('Paiement à 30 jours nets', 'esi_peppol'),
|
||||
$order
|
||||
);
|
||||
|
||||
// Données vendeur (store) - basées sur les options WooCommerce
|
||||
$store_name = \get_bloginfo('name');
|
||||
$store_address = \get_option('woocommerce_store_address', '');
|
||||
$store_address2 = \get_option('woocommerce_store_address_2', '');
|
||||
$store_city = \get_option('woocommerce_store_city', '');
|
||||
$store_postcode = \get_option('woocommerce_store_postcode', '');
|
||||
$store_country = \get_option('woocommerce_default_country', '');
|
||||
$store_vat_number = \get_option('woocommerce_store_vat_number', '');
|
||||
|
||||
// Le country peut être sous forme "BE:BE" selon les versions
|
||||
if (strpos($store_country, ':') !== false) {
|
||||
[$store_country] = explode(':', $store_country, 2);
|
||||
}
|
||||
|
||||
$seller = [
|
||||
'name' => $store_name,
|
||||
'legal_name' => $store_name,
|
||||
'company_id' => '',
|
||||
'vat_number' => $store_vat_number,
|
||||
'address' => [
|
||||
'address_line_1' => $store_address,
|
||||
'address_line_2' => $store_address2,
|
||||
'city' => $store_city,
|
||||
'postal_code' => $store_postcode,
|
||||
'country_code' => $store_country,
|
||||
],
|
||||
'contact' => [
|
||||
'contact_name' => $store_name,
|
||||
'contact_email' => \get_option('admin_email', ''),
|
||||
],
|
||||
];
|
||||
|
||||
$seller = \apply_filters('esi_peppol_seller_party', $seller, $order);
|
||||
|
||||
// Données client (buyer) à partir de la commande
|
||||
$buyer_country = $order->get_billing_country();
|
||||
|
||||
$buyer = [
|
||||
'name' => $order->get_billing_company() ?: $order->get_formatted_billing_full_name(),
|
||||
'legal_name' => $order->get_billing_company() ?: $order->get_formatted_billing_full_name(),
|
||||
'company_id' => '',
|
||||
'vat_number' => $order->get_meta('_billing_vat_number') ?: '',
|
||||
'address' => [
|
||||
'address_line_1' => $order->get_billing_address_1(),
|
||||
'address_line_2' => $order->get_billing_address_2(),
|
||||
'city' => $order->get_billing_city(),
|
||||
'postal_code' => $order->get_billing_postcode(),
|
||||
'country_code' => $buyer_country,
|
||||
],
|
||||
'contact' => [
|
||||
'contact_name' => $order->get_formatted_billing_full_name(),
|
||||
'contact_email' => $order->get_billing_email(),
|
||||
'contact_phone' => $order->get_billing_phone(),
|
||||
],
|
||||
];
|
||||
|
||||
$buyer = \apply_filters('esi_peppol_buyer_party', $buyer, $order);
|
||||
|
||||
// Lignes de facture
|
||||
$invoice_lines = [];
|
||||
$total_excl_vat = 0.0;
|
||||
$total_vat = 0.0;
|
||||
$total_incl_vat = 0.0;
|
||||
$vat_totals_by_rate_code = [];
|
||||
|
||||
$line_number = 1;
|
||||
|
||||
// 1) Lignes produits
|
||||
foreach ($order->get_items('line_item') as $item) {
|
||||
/** @var \WC_Order_Item_Product $item */
|
||||
$product = $item->get_product();
|
||||
|
||||
$quantity = (float) $item->get_quantity();
|
||||
$line_subtotal = (float) $item->get_subtotal();
|
||||
$line_subtotal_tax = (float) $item->get_subtotal_tax();
|
||||
$line_total = (float) $item->get_total();
|
||||
$line_total_tax = (float) $item->get_total_tax();
|
||||
|
||||
// On travaille sur les montants "total" (après remises)
|
||||
$taxable_amount = $line_total;
|
||||
$vat_amount = $line_total_tax;
|
||||
$line_total_incl_vat = $taxable_amount + $vat_amount;
|
||||
|
||||
$unit_price = $quantity > 0 ? $taxable_amount / $quantity : 0.0;
|
||||
|
||||
// Détermination du taux de TVA via le helper centralisé
|
||||
$vat_rate = 0.0;
|
||||
$tax_details = PEPPOL_Woo_Helper::my_get_item_tax_details($item);
|
||||
|
||||
if (!empty($tax_details)) {
|
||||
// On prend le premier taux non nul (cas standard WooCommerce)
|
||||
foreach ($tax_details as $tax_detail) {
|
||||
if (!empty($tax_detail['percent'])) {
|
||||
$vat_rate = round((float) $tax_detail['percent'], 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: approximation du taux de TVA basée sur les montants
|
||||
if ($vat_rate === 0.0) {
|
||||
if ($line_subtotal > 0.0 && $line_subtotal_tax > 0.0) {
|
||||
$vat_rate = ($line_subtotal_tax / $line_subtotal) * 100;
|
||||
} elseif ($taxable_amount > 0.0 && $vat_amount > 0.0) {
|
||||
$vat_rate = ($vat_amount / $taxable_amount) * 100;
|
||||
}
|
||||
|
||||
$vat_rate = round($vat_rate, 2);
|
||||
}
|
||||
|
||||
// Log technique pour debug TVA par ligne
|
||||
if (class_exists(PEPPOL_Plugin::class)) {
|
||||
PEPPOL_Plugin::write_debug_file(
|
||||
[
|
||||
'event' => 'vat_debug_line',
|
||||
'order_id' => $order->get_id(),
|
||||
'line_number' => $line_number,
|
||||
'product_id' => $product ? $product->get_id() : null,
|
||||
'product_name' => $item->get_name(),
|
||||
'quantity' => $quantity,
|
||||
'line_subtotal' => $line_subtotal,
|
||||
'line_subtotal_tax' => $line_subtotal_tax,
|
||||
'line_total' => $line_total,
|
||||
'line_total_tax' => $line_total_tax,
|
||||
'taxable_amount' => $taxable_amount,
|
||||
'vat_amount' => $vat_amount,
|
||||
'computed_vat_rate' => $vat_rate,
|
||||
'line_taxes_raw' => $item->get_taxes(),
|
||||
'tax_details_helper' => $tax_details,
|
||||
],
|
||||
'INFO'
|
||||
);
|
||||
}
|
||||
|
||||
// Déterminer la catégorie de TVA en fonction du montant de TVA
|
||||
// - Montant TVA 0 => catégorie "E" (exempt / taux zéro appliqué)
|
||||
// - Montant TVA >0 => catégorie "S" (standard)
|
||||
$is_zero_vat_amount = ($vat_amount == 0.0);
|
||||
$vat_category_code = $is_zero_vat_amount ? 'E' : 'S';
|
||||
|
||||
$unit_of_measure = 'C62'; // "Unit" code par défaut
|
||||
|
||||
if ($product && $product->get_meta('unit_of_measure')) {
|
||||
$unit_of_measure = (string) $product->get_meta('unit_of_measure');
|
||||
}
|
||||
|
||||
$seller_item_id = '';
|
||||
if ($product) {
|
||||
// Pour les produits : P_ + SKU si existant, sinon P_ + ID produit
|
||||
$base_id = $product->get_sku() ?: (string) $product->get_id();
|
||||
$seller_item_id = 'P_' . $base_id;
|
||||
}
|
||||
|
||||
// Description courte sans balises HTML pour compatibilité PEPPOL
|
||||
$item_description = '';
|
||||
if ($product) {
|
||||
$raw_short_description = (string) $product->get_short_description();
|
||||
// Supprime toutes les balises HTML et compresse les espaces
|
||||
$item_description = \wp_strip_all_tags($raw_short_description, true);
|
||||
}
|
||||
|
||||
$line = [
|
||||
'line_number' => $line_number,
|
||||
'item_name' => $item->get_name(),
|
||||
'item_description' => $item_description,
|
||||
'seller_item_id' => $seller_item_id,
|
||||
'quantity' => $quantity,
|
||||
'unit_of_measure' => $unit_of_measure,
|
||||
'unit_price' => round($unit_price, 2),
|
||||
'vat_rate' => $vat_rate,
|
||||
'vat_category_code' => $vat_category_code,
|
||||
'line_total_amount' => round($taxable_amount, 2),
|
||||
'line_vat_amount' => round($vat_amount, 2),
|
||||
'line_total_amount_including_vat' => round($line_total_incl_vat, 2),
|
||||
];
|
||||
|
||||
$invoice_lines[] = \apply_filters('esi_peppol_invoice_line', $line, $item, $order);
|
||||
|
||||
$total_excl_vat += $taxable_amount;
|
||||
$total_vat += $vat_amount;
|
||||
$total_incl_vat += $line_total_incl_vat;
|
||||
|
||||
self::accumulate_vat_totals_for_line(
|
||||
$vat_totals_by_rate_code,
|
||||
$taxable_amount,
|
||||
$vat_amount,
|
||||
$tax_details,
|
||||
$vat_rate
|
||||
);
|
||||
|
||||
++$line_number;
|
||||
}
|
||||
|
||||
// 2) Lignes de livraison
|
||||
foreach ($order->get_items('shipping') as $item) {
|
||||
/** @var \WC_Order_Item_Shipping $item */
|
||||
$quantity = 1.0;
|
||||
|
||||
// IMPORTANT : pour la livraison, on recalcule la base HT à partir du montant TTC
|
||||
// et du taux de TVA réel, car les valeurs de WooCommerce (get_total_tax()) peuvent
|
||||
// être incohérentes avec le taux de TVA appliqué.
|
||||
//
|
||||
// Exemple : si total TTC = 50 € et taux TVA = 21% :
|
||||
// - Base HT correcte = 50 / 1.21 = 41.32 €
|
||||
// - TVA correcte = 50 - 41.32 = 8.68 €
|
||||
//
|
||||
// (et NON pas : 50 - 10.5 = 39.5 € comme le donnerait une simple soustraction
|
||||
// de la TVA retournée par WooCommerce, qui peut être erronée)
|
||||
|
||||
$gross_amount = (float) $item->get_total(); // Montant TVAC
|
||||
$line_total_incl_vat = $gross_amount;
|
||||
|
||||
// Détermination du taux de TVA pour la livraison via le helper
|
||||
$vat_rate = 0.0;
|
||||
$tax_details = PEPPOL_Woo_Helper::my_get_item_tax_details($item);
|
||||
|
||||
if (!empty($tax_details)) {
|
||||
foreach ($tax_details as $tax_detail) {
|
||||
if (!empty($tax_detail['percent'])) {
|
||||
$vat_rate = round((float) $tax_detail['percent'], 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calcul de la base HT et de la TVA à partir du montant TVAC et du taux.
|
||||
// Pour un taux de 21%, on fait : TVAC / 1.21 pour obtenir la base HT,
|
||||
// puis TVA = TVAC - HT.
|
||||
if ($vat_rate > 0.0) {
|
||||
$divider = 1 + ($vat_rate / 100);
|
||||
$taxable_amount = $divider > 0 ? $gross_amount / $divider : $gross_amount;
|
||||
$vat_amount = $gross_amount - $taxable_amount;
|
||||
} else {
|
||||
// Si pas de taux de TVA détecté, on considère tout en HT.
|
||||
$taxable_amount = $gross_amount;
|
||||
$vat_amount = 0.0;
|
||||
}
|
||||
|
||||
// Sécurité : éviter les valeurs négatives en cas de configuration exotique.
|
||||
if ($taxable_amount < 0) {
|
||||
$taxable_amount = 0.0;
|
||||
}
|
||||
|
||||
// Arrondis pour cohérence avec les autres lignes
|
||||
$taxable_amount = round($taxable_amount, 2);
|
||||
$vat_amount = round($vat_amount, 2);
|
||||
|
||||
$unit_price = $quantity > 0 ? $taxable_amount / $quantity : 0.0;
|
||||
|
||||
$is_zero_vat_amount = ($vat_amount == 0.0);
|
||||
$vat_category_code = $is_zero_vat_amount ? 'E' : 'S';
|
||||
|
||||
// Identifiant vendeur pour les frais de livraison : préfixe SHIPCQT_ + n° de ligne
|
||||
$seller_item_id = 'SHIPCQT_' . $line_number;
|
||||
|
||||
$line = [
|
||||
'line_number' => $line_number,
|
||||
'item_name' => $item->get_name() ?: \__('Frais de livraison', 'esi_peppol'),
|
||||
'item_description' => '',
|
||||
'seller_item_id' => $seller_item_id,
|
||||
'quantity' => $quantity,
|
||||
'unit_of_measure' => 'C62',
|
||||
'unit_price' => round($unit_price, 2),
|
||||
'vat_rate' => $vat_rate,
|
||||
'vat_category_code' => $vat_category_code,
|
||||
'line_total_amount' => $taxable_amount,
|
||||
'line_vat_amount' => $vat_amount,
|
||||
'line_total_amount_including_vat' => round($line_total_incl_vat, 2),
|
||||
];
|
||||
|
||||
$invoice_lines[] = \apply_filters('esi_peppol_invoice_line_shipping', $line, $item, $order);
|
||||
|
||||
$total_excl_vat += $taxable_amount;
|
||||
$total_vat += $vat_amount;
|
||||
$total_incl_vat += $line_total_incl_vat;
|
||||
|
||||
self::accumulate_vat_totals_for_line(
|
||||
$vat_totals_by_rate_code,
|
||||
$taxable_amount,
|
||||
$vat_amount,
|
||||
$tax_details,
|
||||
$vat_rate
|
||||
);
|
||||
|
||||
++$line_number;
|
||||
}
|
||||
|
||||
// 3) Lignes de frais supplémentaires
|
||||
foreach ($order->get_items('fee') as $item) {
|
||||
/** @var \WC_Order_Item_Fee $item */
|
||||
$quantity = 1.0;
|
||||
|
||||
// Montant du fee dans WooCommerce.
|
||||
// Pour les frais, on considère que get_total() retourne un montant TVAC
|
||||
// et on reconstitue la base HT & la TVA à partir du taux de TVA.
|
||||
$gross_amount = (float) $item->get_total(); // Montant TVAC
|
||||
|
||||
// Détermination du taux de TVA pour les frais via le helper
|
||||
$vat_rate = 0.0;
|
||||
$tax_details = PEPPOL_Woo_Helper::my_get_item_tax_details($item);
|
||||
|
||||
if (!empty($tax_details)) {
|
||||
foreach ($tax_details as $tax_detail) {
|
||||
if (!empty($tax_detail['percent'])) {
|
||||
$vat_rate = round((float) $tax_detail['percent'], 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calcul de la base HT et de la TVA à partir du montant TVAC.
|
||||
// Exemple : pour 21%, on fait TVAC / 1.21 pour obtenir la base,
|
||||
// puis TVA = TVAC - HT.
|
||||
if ($vat_rate > 0.0) {
|
||||
$divider = 1 + ($vat_rate / 100);
|
||||
$taxable_amount = $divider > 0 ? $gross_amount / $divider : $gross_amount;
|
||||
$vat_amount = $gross_amount - $taxable_amount;
|
||||
} else {
|
||||
// Si pas de taux de TVA détecté, on considère tout en HT.
|
||||
$taxable_amount = $gross_amount;
|
||||
$vat_amount = 0.0;
|
||||
}
|
||||
|
||||
$line_total_incl_vat = $gross_amount;
|
||||
|
||||
if ($taxable_amount < 0) {
|
||||
$taxable_amount = 0.0;
|
||||
}
|
||||
|
||||
// Arrondis pour cohérence
|
||||
$taxable_amount = round($taxable_amount, 2);
|
||||
$vat_amount = round($vat_amount, 2);
|
||||
|
||||
$unit_price = $quantity > 0 ? $taxable_amount / $quantity : 0.0;
|
||||
|
||||
$is_zero_vat_amount = ($vat_amount == 0.0);
|
||||
$vat_category_code = $is_zero_vat_amount ? 'E' : 'S';
|
||||
|
||||
// Identifiant vendeur pour les frais supplémentaires : préfixe FEE_ + n° de ligne
|
||||
$seller_item_id = 'FEE_' . $line_number;
|
||||
|
||||
$line = [
|
||||
'line_number' => $line_number,
|
||||
'item_name' => $item->get_name() ?: \__('Frais supplémentaires', 'esi_peppol'),
|
||||
'item_description' => '',
|
||||
'seller_item_id' => $seller_item_id,
|
||||
'quantity' => $quantity,
|
||||
'unit_of_measure' => 'C62',
|
||||
'unit_price' => round($unit_price, 2),
|
||||
'vat_rate' => $vat_rate,
|
||||
'vat_category_code' => $vat_category_code,
|
||||
'line_total_amount' => $taxable_amount,
|
||||
'line_vat_amount' => $vat_amount,
|
||||
'line_total_amount_including_vat' => round($line_total_incl_vat, 2),
|
||||
];
|
||||
|
||||
$invoice_lines[] = \apply_filters('esi_peppol_invoice_line_fee', $line, $item, $order);
|
||||
|
||||
$total_excl_vat += $taxable_amount;
|
||||
$total_vat += $vat_amount;
|
||||
$total_incl_vat += $line_total_incl_vat;
|
||||
|
||||
self::accumulate_vat_totals_for_line(
|
||||
$vat_totals_by_rate_code,
|
||||
$taxable_amount,
|
||||
$vat_amount,
|
||||
$tax_details,
|
||||
$vat_rate
|
||||
);
|
||||
|
||||
++$line_number;
|
||||
}
|
||||
|
||||
$vat_totals = array_map(
|
||||
static function (array $row) use ($order): array {
|
||||
// Arrondir la base imposable agrégée
|
||||
$row['taxable_amount'] = round($row['taxable_amount'], 2);
|
||||
|
||||
// IMPORTANT : recalculer la TVA à partir de la base totale et du taux,
|
||||
// au lieu de sommer les montants de TVA ligne par ligne (produits, frais, etc.).
|
||||
// Cela garantit que la TVA par taux est cohérente avec la base globale.
|
||||
$rate = isset($row['vat_rate']) ? (float) $row['vat_rate'] : 0.0;
|
||||
if ($rate > 0.0) {
|
||||
$row['vat_amount'] = round($row['taxable_amount'] * $rate / 100, 2);
|
||||
} else {
|
||||
$row['vat_amount'] = 0.0;
|
||||
}
|
||||
|
||||
// Catégorie "E" si aucun montant de TVA pour ce taux, sinon "S"
|
||||
$row['vat_category_code'] = ($row['vat_amount'] == 0.0) ? 'E' : 'S';
|
||||
|
||||
// Log technique pour debug TVA agrégée par taux
|
||||
if (class_exists(PEPPOL_Plugin::class)) {
|
||||
\ESI_PEPPOL\controllers\PEPPOL_Plugin::write_debug_file(
|
||||
[
|
||||
'event' => 'vat_debug_total',
|
||||
'order_id' => $order->get_id(),
|
||||
'vat_rate' => $row['vat_rate'],
|
||||
'taxable_amount' => $row['taxable_amount'],
|
||||
'vat_amount' => $row['vat_amount'],
|
||||
'vat_category_code' => $row['vat_category_code'],
|
||||
],
|
||||
'INFO'
|
||||
);
|
||||
}
|
||||
|
||||
return $row;
|
||||
},
|
||||
$vat_totals_by_rate_code
|
||||
);
|
||||
|
||||
// Nettoyage : on supprime les lignes de TVA sans montant (taux 0% "artificiel")
|
||||
// afin d'éviter d'avoir un bucket 0% dans vat_totals quand toute la facture
|
||||
// est réellement soumise à un taux positif (ex. 21% partout).
|
||||
$vat_totals = array_filter(
|
||||
$vat_totals,
|
||||
static function (array $row): bool {
|
||||
return isset($row['vat_amount']) && (float) $row['vat_amount'] > 0.0;
|
||||
}
|
||||
);
|
||||
// Réindexer les clés après le filtre
|
||||
$vat_totals = array_values($vat_totals);
|
||||
|
||||
$invoice_totals = [
|
||||
'total_amount_excluding_vat' => round($total_excl_vat, 2),
|
||||
'total_vat_amount' => round($total_vat, 2),
|
||||
'total_amount_including_vat' => round($total_incl_vat, 2),
|
||||
'total_paid_amount' => 0.0,
|
||||
'total_payable_amount' => round($order->get_total(), 2),
|
||||
'amount_due' => round($order->get_total(), 2),
|
||||
];
|
||||
|
||||
$invoice_totals = \apply_filters('esi_peppol_invoice_totals', $invoice_totals, $order);
|
||||
|
||||
// Informations de paiement (laissées filtrables pour configuration future)
|
||||
$payment_info = [
|
||||
'payee_iban' => '',
|
||||
'payee_bic' => '',
|
||||
'bank_name' => '',
|
||||
];
|
||||
|
||||
$payment_info = \apply_filters('esi_peppol_payment_info', $payment_info, $order);
|
||||
|
||||
$payload = [
|
||||
'document_type' => 'invoice',
|
||||
'external_reference' => $external_reference,
|
||||
'invoice' => [
|
||||
'invoice_number' => $order_number,
|
||||
'invoice_date' => $invoice_date,
|
||||
'due_date' => $due_date,
|
||||
'currency_code' => $currency_code,
|
||||
'invoice_notes' => $invoice_notes,
|
||||
'payment_terms' => $payment_terms,
|
||||
],
|
||||
'parties' => [
|
||||
'seller' => $seller,
|
||||
'buyer' => $buyer,
|
||||
],
|
||||
'invoice_lines' => $invoice_lines,
|
||||
'vat_totals' => array_values($vat_totals),
|
||||
'invoice_totals' => $invoice_totals,
|
||||
'payment_info' => $payment_info,
|
||||
];
|
||||
|
||||
return \apply_filters('esi_peppol_payload_from_order', $payload, $order);
|
||||
}
|
||||
}
|
||||
710
app/controllers/Plugin.php
Normal file
710
app/controllers/Plugin.php
Normal file
@ -0,0 +1,710 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_PEPPOL\controllers;
|
||||
|
||||
use ESI_PEPPOL\models\PEPPOL_Main_model;
|
||||
|
||||
class PEPPOL_Plugin {
|
||||
|
||||
|
||||
public function init() {
|
||||
$this->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`.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate(): void {
|
||||
// Création / mise à jour de la table principale des factures
|
||||
PEPPOL_Main_model::create_table();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 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);
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
// 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
|
||||
$is_peppol_page = isset($_GET['page']) && strpos((string) $_GET['page'], 'esi-peppol') === 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if (!$is_peppol_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)
|
||||
wp_enqueue_style(
|
||||
'esi-peppol-datatables',
|
||||
ESI_PEPPOL_URL . 'assets/css/datatables' . $suffix . '.css',
|
||||
['esi-peppol-admin'],
|
||||
ESI_PEPPOL_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'esi-peppol-datatables',
|
||||
ESI_PEPPOL_URL . 'assets/js/datatables' . $suffix . '.js',
|
||||
['jquery'],
|
||||
ESI_PEPPOL_VERSION,
|
||||
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'),
|
||||
'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'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* - 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.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_vat_notice_message(): ?string {
|
||||
$vat_number = (string) get_option('woocommerce_store_vat_number', '');
|
||||
|
||||
if ($vat_number !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$api_key = (string) get_option('esi_peppol_api_key', '');
|
||||
$password = (string) get_option('esi_peppol_password', '');
|
||||
|
||||
if ($api_key === '' && $password === '') {
|
||||
// Invitation initiale
|
||||
return __(
|
||||
'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'
|
||||
);
|
||||
}
|
||||
|
||||
// Rappel après configuration du connecteur
|
||||
return __(
|
||||
'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'
|
||||
);
|
||||
}
|
||||
|
||||
public static function write_log($message, $level = 'INFO') {
|
||||
$message = self::format_message_for_log($message);
|
||||
$log_entry = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message);
|
||||
error_log($log_entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Écrit un message dans un fichier debug.txt sous le dossier uploads
|
||||
* Chemin: wp-content/uploads/crvi-debug.txt
|
||||
*/
|
||||
public static function write_debug_file($message, $level = 'INFO') {
|
||||
$message = self::format_message_for_log($message);
|
||||
$log_entry = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message);
|
||||
|
||||
$upload_dir = wp_upload_dir();
|
||||
$file_path = trailingslashit($upload_dir['basedir']) . 'crvi-debug.txt';
|
||||
|
||||
// Tente d'écrire le log. En cas d'échec, fallback sur error_log
|
||||
@file_put_contents($file_path, $log_entry, FILE_APPEND);
|
||||
|
||||
// Écrire également dans le log PHP (wp-content/debug.log si WP_DEBUG_LOG actif, sinon error_log du serveur)
|
||||
error_log($log_entry);
|
||||
}
|
||||
|
||||
private static function format_message_for_log($message) {
|
||||
if (is_array($message)) {
|
||||
return print_r($message, true);
|
||||
} elseif (is_object($message)) {
|
||||
return print_r($message, true);
|
||||
} elseif (is_bool($message)) {
|
||||
return $message ? 'TRUE' : 'FALSE';
|
||||
} elseif (is_numeric($message)) {
|
||||
return (string) $message;
|
||||
} elseif (is_null($message)) {
|
||||
return 'NULL';
|
||||
} else {
|
||||
return var_export($message, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un email de notification en cas d'erreur API Peppol.
|
||||
*
|
||||
* @param int $order_id ID de la commande WooCommerce.
|
||||
* @param array $api_result Résultat de l'appel API (success, http_code, message, data).
|
||||
* @param array $payload Payload envoyé à l'API (optionnel, pour debug).
|
||||
*
|
||||
* @return bool True si l'email a été envoyé avec succès, false sinon.
|
||||
*/
|
||||
public static function send_api_error_email(int $order_id, array $api_result, array $payload = []): bool {
|
||||
// Vérifier si l'email est configuré
|
||||
$email = \get_option('esi_peppol_email', '');
|
||||
if (empty($email) || !\is_email($email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Récupérer la commande
|
||||
$order = \wc_get_order($order_id);
|
||||
if (!$order instanceof \WC_Order) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Préparer les variables pour le template
|
||||
$order_number = $order->get_order_number();
|
||||
$http_code = isset($api_result['http_code']) ? (int) $api_result['http_code'] : 0;
|
||||
$error_message = isset($api_result['message']) && $api_result['message'] !== ''
|
||||
? (string) $api_result['message']
|
||||
: \__('Erreur inconnue lors de l\'envoi vers l\'API Peppol.', 'esi_peppol');
|
||||
|
||||
// Extraire les données d'erreur supplémentaires
|
||||
$error_data = [];
|
||||
if (isset($api_result['data']) && is_array($api_result['data'])) {
|
||||
$error_data = $api_result['data'];
|
||||
}
|
||||
// Si la structure error existe directement dans api_result, l'inclure aussi
|
||||
if (isset($api_result['error']) && is_array($api_result['error'])) {
|
||||
$error_data['error'] = $api_result['error'];
|
||||
// Si le message d'erreur n'est pas défini, utiliser celui de error.message
|
||||
if (empty($error_message) && isset($api_result['error']['message'])) {
|
||||
$error_message = (string) $api_result['error']['message'];
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer le template email
|
||||
$template_path = ESI_PEPPOL_DIR . 'templates/email/api-error.php';
|
||||
if (!file_exists($template_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Capturer le contenu du template
|
||||
ob_start();
|
||||
include $template_path;
|
||||
$email_body = ob_get_clean();
|
||||
|
||||
// Préparer les en-têtes de l'email
|
||||
$site_name = \get_bloginfo('name');
|
||||
$headers = [
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'From: ' . $site_name . ' <' . \get_option('admin_email') . '>',
|
||||
];
|
||||
|
||||
// Sujet de l'email
|
||||
$subject = \sprintf(
|
||||
/* translators: %1$s: site name, %2$s: order number */
|
||||
\__('[%1$s] Erreur API Peppol - Commande %2$s', 'esi_peppol'),
|
||||
$site_name,
|
||||
$order_number
|
||||
);
|
||||
|
||||
// Envoyer l'email
|
||||
$sent = \wp_mail($email, $subject, $email_body, $headers);
|
||||
|
||||
// Log de l'envoi
|
||||
if ($sent) {
|
||||
self::write_debug_file(
|
||||
[
|
||||
'event' => 'api_error_email_sent',
|
||||
'order_id' => $order_id,
|
||||
'email' => $email,
|
||||
'http_code' => $http_code,
|
||||
'error_message' => $error_message,
|
||||
],
|
||||
'INFO'
|
||||
);
|
||||
} else {
|
||||
self::write_debug_file(
|
||||
[
|
||||
'event' => 'api_error_email_failed',
|
||||
'order_id' => $order_id,
|
||||
'email' => $email,
|
||||
'error' => 'wp_mail returned false',
|
||||
],
|
||||
'ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
return $sent;
|
||||
}
|
||||
|
||||
}
|
||||
206
app/controllers/Woocommerce_controller.php
Normal file
206
app/controllers/Woocommerce_controller.php
Normal file
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_PEPPOL\controllers;
|
||||
|
||||
class PEPPOL_Woocommerce_controller {
|
||||
|
||||
/**
|
||||
* Enregistre les hooks WooCommerce nécessaires.
|
||||
*
|
||||
* À appeler depuis le contrôleur principal du plugin.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_hooks(): void {
|
||||
// Déclenché lorsque le paiement d'une commande est complété
|
||||
\add_action(
|
||||
'woocommerce_payment_complete',
|
||||
[self::class, 'post_payment'],
|
||||
10,
|
||||
1
|
||||
);
|
||||
|
||||
// Sécurité : on écoute également le passage au statut "completed"
|
||||
\add_action(
|
||||
'woocommerce_order_status_completed',
|
||||
[self::class, 'post_payment'],
|
||||
10,
|
||||
1
|
||||
);
|
||||
|
||||
// Alerte admin si BACS est activé mais sans comptes bancaires renseignés
|
||||
\add_action(
|
||||
'admin_notices',
|
||||
[self::class, 'maybe_show_bacs_notice']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback déclenché après le paiement ou lors du passage
|
||||
* d'une commande au statut "terminée".
|
||||
*
|
||||
* @param int $order_id ID de la commande WooCommerce.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function post_payment(int $order_id): void {
|
||||
$order_id = (int) $order_id;
|
||||
|
||||
if ($order_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = \wc_get_order($order_id);
|
||||
|
||||
if (!$order instanceof \WC_Order) {
|
||||
return;
|
||||
}
|
||||
|
||||
// N'envoyer vers Peppol que les commandes avec un numéro de TVA saisi ET valide.
|
||||
// Le plugin "WooCommerce EU VAT Number" stocke le numéro validé dans la meta "_billing_vat_number"
|
||||
// et le statut de validation dans "_vat_number_is_valid" (valeur 'true' ou 'false').
|
||||
$billing_vat_number = (string) $order->get_meta('_billing_vat_number');
|
||||
$vat_is_valid = (string) $order->get_meta('_vat_number_is_valid');
|
||||
|
||||
if ($billing_vat_number === '' || $vat_is_valid !== 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Éviter les envois multiples si déjà traité
|
||||
$already_sent = $order->get_meta('_esi_peppol_sent_at');
|
||||
if (!empty($already_sent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construire le payload JSON à partir de la commande
|
||||
$payload = PEPPOL_peppol_controller::build_payload_from_order($order);
|
||||
|
||||
/* echo '<pre>';
|
||||
print_r($payload);
|
||||
echo '</pre>';
|
||||
die(); */
|
||||
|
||||
// Appel API ESIPeppol + enregistrement dans la table custom
|
||||
$result = PEPPOL_peppol_controller::upload_json($payload, $order_id);
|
||||
|
||||
// Mémoriser la date d'envoi, même en cas d'échec pour éviter les boucles
|
||||
$order->update_meta_data('_esi_peppol_sent_at', \current_time('mysql'));
|
||||
$order->update_meta_data('_esi_peppol_last_peppol_status', $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();
|
||||
|
||||
// Log technique pour debugging
|
||||
if (class_exists(PEPPOL_Plugin::class)) {
|
||||
PEPPOL_Plugin::write_debug_file(
|
||||
[
|
||||
'event' => 'post_payment',
|
||||
'order_id' => $order_id,
|
||||
'success' => $result['success'] ?? false,
|
||||
'http_code' => $result['http_code'] ?? null,
|
||||
'message' => $result['message'] ?? '',
|
||||
'api_response' => $result['data'] ?? null,
|
||||
],
|
||||
$result['success'] ? 'INFO' : 'ERROR'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'état du mode de paiement BACS (virement bancaire)
|
||||
* et la présence de comptes bancaires configurés.
|
||||
*
|
||||
* @return array{
|
||||
* installed: bool,
|
||||
* enabled: bool,
|
||||
* has_accounts: bool,
|
||||
* valid_accounts_count: int
|
||||
* }
|
||||
*/
|
||||
public static function my_check_bacs_status_and_accounts(): array {
|
||||
$gateways = \WC_Payment_Gateways::instance()->payment_gateways();
|
||||
|
||||
if (empty($gateways['bacs'])) {
|
||||
return [
|
||||
'installed' => false,
|
||||
'enabled' => false,
|
||||
'has_accounts' => false,
|
||||
'valid_accounts_count'=> 0,
|
||||
];
|
||||
}
|
||||
|
||||
/** @var \WC_Gateway_BACS $bacs */
|
||||
$bacs = $gateways['bacs'];
|
||||
|
||||
// 1) Est-ce que BACS est activé ?
|
||||
$enabled = ($bacs->enabled === 'yes');
|
||||
|
||||
// 2) Est-ce qu'il y a des comptes renseignés ?
|
||||
// Les comptes sont stockés dans l'option "woocommerce_bacs_accounts"
|
||||
$accounts = \get_option('woocommerce_bacs_accounts', []);
|
||||
$has_accounts = !empty($accounts);
|
||||
|
||||
// Optionnel : vérifier qu'un compte contient bien les champs minimum
|
||||
$valid_accounts = array_filter(
|
||||
$accounts,
|
||||
static function ($acc) {
|
||||
return !empty($acc['account_name'])
|
||||
&& (!empty($acc['iban']) || !empty($acc['account_number']))
|
||||
&& !empty($acc['bank_name']);
|
||||
}
|
||||
);
|
||||
|
||||
return [
|
||||
'installed' => true,
|
||||
'enabled' => $enabled,
|
||||
'has_accounts' => $has_accounts,
|
||||
'valid_accounts_count' => count($valid_accounts),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche un message d'avertissement dans l'admin WooCommerce
|
||||
* si BACS est activé mais qu'aucun compte bancaire n'est renseigné.
|
||||
*
|
||||
* Hooké sur admin_notices.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function maybe_show_bacs_notice(): void {
|
||||
if (!\current_user_can('manage_woocommerce')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = \function_exists('get_current_screen') ? \get_current_screen() : null;
|
||||
|
||||
// On cible la page WooCommerce > Réglages > Paiements
|
||||
if (!$screen || (string) $screen->id !== 'woocommerce_page_wc-settings') {
|
||||
return;
|
||||
}
|
||||
|
||||
$tab = isset($_GET['tab']) ? \sanitize_text_field(\wp_unslash($_GET['tab'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ($tab !== 'checkout') {
|
||||
return;
|
||||
}
|
||||
|
||||
$status = self::my_check_bacs_status_and_accounts();
|
||||
|
||||
// Si BACS n'est pas installé ou pas activé, on ne dit rien
|
||||
if (empty($status['installed']) || empty($status['enabled'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si au moins un compte valide est configuré, pas de message
|
||||
if (!empty($status['valid_accounts_count'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="notice notice-warning"><p>' .
|
||||
\esc_html__(
|
||||
'Le mode de paiement virement bancaire (BACS) est activé mais aucun compte bancaire complet n\'est renseigné. Veuillez configurer au moins un compte (titulaire, IBAN ou numéro de compte, banque) pour afficher les instructions de paiement à vos clients.',
|
||||
'esi_peppol'
|
||||
) .
|
||||
'</p></div>';
|
||||
}
|
||||
|
||||
}
|
||||
31
app/helpers/Woo_Helper.php
Normal file
31
app/helpers/Woo_Helper.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_PEPPOL\helpers;
|
||||
|
||||
class PEPPOL_Woo_Helper {
|
||||
|
||||
public static function my_get_item_tax_details( \WC_Order_Item $item ): array {
|
||||
$taxes = $item->get_taxes(); // ['total' => [rate_id => amount], 'subtotal' => ...]
|
||||
$out = [];
|
||||
|
||||
if ( ! empty( $taxes['total'] ) ) {
|
||||
foreach ( $taxes['total'] as $rate_id => $tax_amount ) {
|
||||
$tax_amount = floatval( $tax_amount );
|
||||
if ( $tax_amount == 0 ) continue;
|
||||
|
||||
$rate = \WC_Tax::_get_tax_rate( $rate_id );
|
||||
|
||||
$out[] = [
|
||||
'rate_id' => (int) $rate_id,
|
||||
'percent' => floatval( $rate['tax_rate'] ),
|
||||
'name' => $rate['tax_rate_name'],
|
||||
'class' => $rate['tax_rate_class'], // 'standard', 'reduced-rate', etc.
|
||||
'amount' => $tax_amount,
|
||||
'compound'=> (bool) $rate['tax_rate_compound'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
250
app/models/Main_model.php
Normal file
250
app/models/Main_model.php
Normal file
@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_PEPPOL\models;
|
||||
|
||||
class PEPPOL_Main_model {
|
||||
|
||||
public $id;
|
||||
public $id_order;
|
||||
public $data_sent;
|
||||
public $response_data;
|
||||
public $document_id;
|
||||
public $peppol_document_id;
|
||||
public $status;
|
||||
public $success;
|
||||
public $message;
|
||||
public $http_code;
|
||||
public $date_add;
|
||||
public $date_update;
|
||||
|
||||
/**
|
||||
* Retourne le nom complet de la table (avec préfixe WP)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_table_name(): string {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->prefix . 'esi_peppol_invoices';
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les derniers enregistrements pour l'affichage dans l'admin.
|
||||
*
|
||||
* @param int $limit Nombre maximum de lignes à retourner.
|
||||
*
|
||||
* @return array Liste d'objets (stdClass).
|
||||
*/
|
||||
public static function get_recent(int $limit = 50): array {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
$limit = max(1, $limit);
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"SELECT * FROM {$table_name} ORDER BY date_add DESC LIMIT %d",
|
||||
$limit
|
||||
);
|
||||
|
||||
return (array) $wpdb->get_results($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée la table esi_peppol_invoices si elle n'existe pas
|
||||
*
|
||||
* À appeler lors de l'activation du plugin.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function create_table(): void {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE {$table_name} (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
id_order BIGINT(20) UNSIGNED NOT NULL,
|
||||
data_sent LONGTEXT NULL,
|
||||
response_data LONGTEXT NULL,
|
||||
document_id VARCHAR(100) NOT NULL,
|
||||
peppol_document_id VARCHAR(191) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
success TINYINT(1) NOT NULL DEFAULT 0,
|
||||
message TEXT NULL,
|
||||
http_code INT(11) NULL,
|
||||
date_add DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
date_update DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY id_order (id_order),
|
||||
KEY document_id (document_id),
|
||||
KEY peppol_document_id (peppol_document_id)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once \ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
\dbDelta($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un enregistrement PEPPOL pour une commande WooCommerce.
|
||||
*
|
||||
* @param int $order_id ID de la commande WooCommerce.
|
||||
* @param mixed $response_data Données retournées par l'API (array / objet / string).
|
||||
* @param mixed $data_sent Payload envoyé à l'API (array / objet / string).
|
||||
* @param string $document_id Identifiant / numéro du document (côté boutique).
|
||||
* @param string $peppol_document_id Identifiant du document dans Peppol.
|
||||
* @param string $status Statut métier (ex: 'created', 'sent', 'error').
|
||||
* @param bool $success Indique si l'appel API est un succès.
|
||||
* @param string $message Message d'information / d'erreur.
|
||||
* @param int|null $http_code Code HTTP retourné par l'API, si disponible.
|
||||
*
|
||||
* @return int|null ID inséré ou null en cas d'erreur.
|
||||
*/
|
||||
public static function create(
|
||||
int $order_id,
|
||||
$response_data,
|
||||
$data_sent,
|
||||
string $document_id,
|
||||
string $peppol_document_id,
|
||||
string $status,
|
||||
bool $success,
|
||||
string $message = '',
|
||||
?int $http_code = null
|
||||
): ?int {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
$serialized_response = \maybe_serialize($response_data);
|
||||
$serialized_data_sent = \maybe_serialize($data_sent);
|
||||
$now = \current_time('mysql');
|
||||
$success_int = $success ? 1 : 0;
|
||||
|
||||
$inserted = $wpdb->insert(
|
||||
$table_name,
|
||||
[
|
||||
'id_order' => $order_id,
|
||||
'data_sent' => $serialized_data_sent,
|
||||
'response_data' => $serialized_response,
|
||||
'document_id' => $document_id,
|
||||
'peppol_document_id' => $peppol_document_id,
|
||||
'status' => $status,
|
||||
'success' => $success_int,
|
||||
'message' => $message,
|
||||
'http_code' => $http_code,
|
||||
'date_add' => $now,
|
||||
'date_update' => $now,
|
||||
],
|
||||
['%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%d', '%s', '%s']
|
||||
);
|
||||
|
||||
if ($inserted === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre ou met à jour les données PEPPOL pour une commande WooCommerce.
|
||||
*
|
||||
* - 1 ligne par commande (id_order) : si une ligne existe déjà, elle est mise à jour.
|
||||
* - La réponse d'API est stockée en sérialisé dans response_data.
|
||||
* - Le payload envoyé est stocké dans data_sent.
|
||||
*/
|
||||
public static function save_for_order(
|
||||
int $order_id,
|
||||
$response_data,
|
||||
$data_sent,
|
||||
string $document_id,
|
||||
string $peppol_document_id,
|
||||
string $status,
|
||||
bool $success,
|
||||
string $message = '',
|
||||
?int $http_code = null
|
||||
): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
$serialized_response = \maybe_serialize($response_data);
|
||||
$serialized_data_sent = \maybe_serialize($data_sent);
|
||||
$now = \current_time('mysql');
|
||||
$success_int = $success ? 1 : 0;
|
||||
|
||||
// Vérifier si une ligne existe déjà pour cette commande
|
||||
$existing_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table_name} WHERE id_order = %d LIMIT 1",
|
||||
$order_id
|
||||
)
|
||||
);
|
||||
|
||||
if ($existing_id) {
|
||||
// Mise à jour
|
||||
$updated = $wpdb->update(
|
||||
$table_name,
|
||||
[
|
||||
'data_sent' => $serialized_data_sent,
|
||||
'response_data' => $serialized_response,
|
||||
'document_id' => $document_id,
|
||||
'peppol_document_id' => $peppol_document_id,
|
||||
'status' => $status,
|
||||
'success' => $success_int,
|
||||
'message' => $message,
|
||||
'http_code' => $http_code,
|
||||
'date_update' => $now,
|
||||
],
|
||||
['id' => (int) $existing_id],
|
||||
['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%d', '%s'],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $updated !== false;
|
||||
}
|
||||
|
||||
// Pas d'enregistrement existant : on crée
|
||||
return self::create(
|
||||
$order_id,
|
||||
$response_data,
|
||||
$data_sent,
|
||||
$document_id,
|
||||
$peppol_document_id,
|
||||
$status,
|
||||
$success,
|
||||
$message,
|
||||
$http_code
|
||||
) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un enregistrement PEPPOL par ID de commande (id_order).
|
||||
*
|
||||
* La propriété response_data est désérialisée automatiquement.
|
||||
* La propriété data_sent est désérialisée automatiquement.
|
||||
*/
|
||||
public static function get_by_order_id(int $order_id): ?object {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table_name} WHERE id_order = %d LIMIT 1",
|
||||
$order_id
|
||||
)
|
||||
);
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Désérialiser la réponse API et le payload si nécessaire
|
||||
if (isset($row->response_data)) {
|
||||
$row->response_data = \maybe_unserialize($row->response_data);
|
||||
}
|
||||
if (isset($row->data_sent)) {
|
||||
$row->data_sent = \maybe_unserialize($row->data_sent);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
118
assets/css/admin.css
Normal file
118
assets/css/admin.css
Normal file
@ -0,0 +1,118 @@
|
||||
/* Styles d'administration pour le plugin ESI Peppol */
|
||||
|
||||
.esi-peppol-settings-wrap {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.esi-peppol-settings-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
||||
padding: 24px 28px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.esi-peppol-settings-card h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.esi-peppol-settings-card .form-table th {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.esi-peppol-settings-card .form-table td input.regular-text {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.esi-peppol-password-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.esi-peppol-password-wrapper .regular-text {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
#esi-peppol-test-result {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#esi-peppol-test-result .notice {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* Styles pour le logo email */
|
||||
.esi-peppol-logo-preview {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.esi-peppol-logo-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#esi-peppol-remove-logo {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Styles pour les statuts dans les logs */
|
||||
.statut {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Couleurs par défaut pour les statuts communs */
|
||||
.statut-success,
|
||||
.statut-succès,
|
||||
.statut-completed,
|
||||
.statut-complété,
|
||||
.statut-ok,
|
||||
.statut-created {
|
||||
background-color: #10b981;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.statut-error,
|
||||
.statut-erreur,
|
||||
.statut-failed,
|
||||
.statut-échec {
|
||||
background-color: #ef4444;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.statut-pending,
|
||||
.statut-en-attente,
|
||||
.statut-waiting,
|
||||
.statut-en-cours {
|
||||
background-color: #f59e0b;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.statut-processing,
|
||||
.statut-traitement,
|
||||
.statut-sent,
|
||||
.statut-envoyé {
|
||||
background-color: #3b82f6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Statut par défaut pour les valeurs non reconnues */
|
||||
.statut-default {
|
||||
background-color: #6b7280;
|
||||
color: #ffffff;
|
||||
}
|
||||
32
assets/css/datatables.min.css
vendored
Normal file
32
assets/css/datatables.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/img/LOGO-ESI-PEPPOL_LIGHT.png
Normal file
BIN
assets/img/LOGO-ESI-PEPPOL_LIGHT.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/img/Peppol-Master-Gradient.png
Normal file
BIN
assets/img/Peppol-Master-Gradient.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
276
assets/js/admin.js
Normal file
276
assets/js/admin.js
Normal file
@ -0,0 +1,276 @@
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
$(function () {
|
||||
// Bouton de test de connexion sur la page de configuration
|
||||
var $testBtn = $('#esi-peppol-test-connection');
|
||||
if ($testBtn.length && typeof window.esiPeppolAdmin !== 'undefined') {
|
||||
var $result = $('#esi-peppol-test-result');
|
||||
|
||||
$testBtn.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var apiKey = $('#esi_peppol_api_key').val();
|
||||
var password = $('#esi_peppol_password').val();
|
||||
|
||||
$result.removeClass().empty();
|
||||
|
||||
if (!apiKey || !password) {
|
||||
var msgMissing = window.esiPeppolAdmin.i18n_missing || "Veuillez renseigner l'API Key et le Password avant de tester la connexion.";
|
||||
$result
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>' + msgMissing + '</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
$testBtn.prop('disabled', true).addClass('updating-message');
|
||||
|
||||
$.post(window.esiPeppolAdmin.ajax_url, {
|
||||
action: 'esi_peppol_test_connection',
|
||||
nonce: window.esiPeppolAdmin.nonce,
|
||||
api_key: apiKey,
|
||||
password: password
|
||||
})
|
||||
.done(function (response) {
|
||||
if (response && response.success) {
|
||||
var msg = (response.data && response.data.message)
|
||||
? response.data.message
|
||||
: (window.esiPeppolAdmin.i18n_success || "Connexion réussie à l'API ESIPeppol.");
|
||||
|
||||
$result
|
||||
.addClass('notice notice-success is-dismissible')
|
||||
.html('<p>' + msg + '</p>');
|
||||
} else {
|
||||
var errMsg = (response && response.data && response.data.message)
|
||||
? response.data.message
|
||||
: (window.esiPeppolAdmin.i18n_error || 'La connexion a échoué.');
|
||||
|
||||
$result
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>' + errMsg + '</p>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
var msgNetwork = window.esiPeppolAdmin.i18n_network || 'Erreur de communication avec le serveur WordPress.';
|
||||
$result
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>' + msgNetwork + '</p>');
|
||||
})
|
||||
.always(function () {
|
||||
$testBtn.prop('disabled', false).removeClass('updating-message');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// DataTables pour le tableau du journal des échanges (invoices / documents envoyés)
|
||||
var $logsTable = $('#esi-peppol-logs-table');
|
||||
if ($logsTable.length && $.fn.DataTable) {
|
||||
$logsTable.DataTable({
|
||||
pageLength: 25,
|
||||
order: [[0, 'desc']],
|
||||
language: {
|
||||
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/fr-FR.json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Actions AJAX sur la page de logs : renvoyer / vérifier le statut
|
||||
if ($logsTable.length && typeof window.esiPeppolAdmin !== 'undefined') {
|
||||
var $logsResult = $('#esi-peppol-logs-result');
|
||||
|
||||
var showLogsNotice = function (type, title, message, detail) {
|
||||
var classes = 'notice';
|
||||
if (type === 'success') {
|
||||
classes += ' notice-success is-dismissible';
|
||||
} else if (type === 'error') {
|
||||
classes += ' notice-error';
|
||||
} else {
|
||||
classes += ' notice-info';
|
||||
}
|
||||
|
||||
var html = '<div class="' + classes + '">';
|
||||
|
||||
if (title) {
|
||||
html += '<p><strong>' + title + '</strong> ' + (message || '') + '</p>';
|
||||
} else if (message) {
|
||||
html += '<p>' + message + '</p>';
|
||||
}
|
||||
|
||||
if (detail) {
|
||||
html += '<p><em>' + (window.esiPeppolAdmin.i18n_logs_detail_lbl || "Détail retour API") + ':</em> ' + detail + '</p>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
$logsResult
|
||||
.empty()
|
||||
.append(html);
|
||||
};
|
||||
|
||||
$logsTable.on('click', '.esi-peppol-log-resend', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $(this);
|
||||
var orderId = $btn.data('order-id');
|
||||
|
||||
if (!orderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$btn.prop('disabled', true).addClass('updating-message');
|
||||
|
||||
$.post(window.esiPeppolAdmin.ajax_url, {
|
||||
action: 'esi_peppol_resend_invoice',
|
||||
nonce: window.esiPeppolAdmin.logs_nonce_resend,
|
||||
order_id: orderId
|
||||
})
|
||||
.done(function (response) {
|
||||
var data = response && response.data ? response.data : {};
|
||||
var ok = !!(response && response.success);
|
||||
var title = ok
|
||||
? (window.esiPeppolAdmin.i18n_logs_ok_title || 'OK')
|
||||
: (window.esiPeppolAdmin.i18n_logs_ko_title || 'Pas OK');
|
||||
|
||||
var message = data.message || (ok
|
||||
? (window.esiPeppolAdmin.i18n_logs_resend_ok || 'Document renvoyé avec succès.')
|
||||
: (window.esiPeppolAdmin.i18n_logs_resend_ko || 'L\'envoi du document a échoué.'));
|
||||
|
||||
var detail = data.detail || '';
|
||||
|
||||
showLogsNotice(ok ? 'success' : 'error', title, message, detail);
|
||||
})
|
||||
.fail(function () {
|
||||
showLogsNotice(
|
||||
'error',
|
||||
window.esiPeppolAdmin.i18n_logs_ko_title || 'Pas OK',
|
||||
window.esiPeppolAdmin.i18n_network || 'Erreur de communication avec le serveur WordPress.',
|
||||
''
|
||||
);
|
||||
})
|
||||
.always(function () {
|
||||
$btn.prop('disabled', false).removeClass('updating-message');
|
||||
});
|
||||
});
|
||||
|
||||
$logsTable.on('click', '.esi-peppol-log-status', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $(this);
|
||||
var orderId = $btn.data('order-id');
|
||||
|
||||
if (!orderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$btn.prop('disabled', true).addClass('updating-message');
|
||||
|
||||
$.post(window.esiPeppolAdmin.ajax_url, {
|
||||
action: 'esi_peppol_check_invoice_status',
|
||||
nonce: window.esiPeppolAdmin.logs_nonce_status,
|
||||
order_id: orderId
|
||||
})
|
||||
.done(function (response) {
|
||||
var data = response && response.data ? response.data : {};
|
||||
var ok = !!(response && response.success);
|
||||
|
||||
var title = ok
|
||||
? (window.esiPeppolAdmin.i18n_logs_ok_title || 'OK')
|
||||
: (window.esiPeppolAdmin.i18n_logs_ko_title || 'Pas OK');
|
||||
|
||||
var message = data.message || (window.esiPeppolAdmin.i18n_logs_status_lbl || 'Statut actuel du document');
|
||||
var detail = data.detail || '';
|
||||
|
||||
showLogsNotice(ok ? 'success' : 'error', title, message, detail);
|
||||
})
|
||||
.fail(function () {
|
||||
showLogsNotice(
|
||||
'error',
|
||||
window.esiPeppolAdmin.i18n_logs_ko_title || 'Pas OK',
|
||||
window.esiPeppolAdmin.i18n_network || 'Erreur de communication avec le serveur WordPress.',
|
||||
''
|
||||
);
|
||||
})
|
||||
.always(function () {
|
||||
$btn.prop('disabled', false).removeClass('updating-message');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle affichage du mot de passe sur la page de configuration
|
||||
$('.esi-peppol-password-toggle').on('click', function () {
|
||||
var $btn = $(this);
|
||||
var targetSelector = $btn.data('target');
|
||||
var $input = $(targetSelector);
|
||||
|
||||
if (!$input.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var isPassword = $input.attr('type') === 'password';
|
||||
$input.attr('type', isPassword ? 'text' : 'password');
|
||||
|
||||
// Texte du bouton
|
||||
$btn.text(isPassword ? 'Masquer' : 'Afficher');
|
||||
});
|
||||
|
||||
// Gestion du logo email avec la médiathèque WordPress
|
||||
var $uploadLogoBtn = $('#esi-peppol-upload-logo');
|
||||
var $removeLogoBtn = $('#esi-peppol-remove-logo');
|
||||
var $logoPreview = $('#esi-peppol-logo-preview');
|
||||
var $logoIdInput = $('#esi_peppol_logo_email_id');
|
||||
|
||||
if ($uploadLogoBtn.length && typeof wp !== 'undefined' && wp.media) {
|
||||
var mediaUploader;
|
||||
|
||||
$uploadLogoBtn.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Si le sélecteur existe déjà, on le réutilise
|
||||
if (mediaUploader) {
|
||||
mediaUploader.open();
|
||||
return;
|
||||
}
|
||||
|
||||
// Créer le sélecteur de média
|
||||
mediaUploader = wp.media({
|
||||
title: 'Choisir un logo',
|
||||
button: {
|
||||
text: 'Utiliser ce logo'
|
||||
},
|
||||
multiple: false,
|
||||
library: {
|
||||
type: 'image'
|
||||
}
|
||||
});
|
||||
|
||||
// Quand une image est sélectionnée
|
||||
mediaUploader.on('select', function () {
|
||||
var attachment = mediaUploader.state().get('selection').first().toJSON();
|
||||
|
||||
// Mettre à jour l'input hidden avec l'ID
|
||||
$logoIdInput.val(attachment.id);
|
||||
|
||||
// Afficher la preview
|
||||
var imageUrl = attachment.sizes && attachment.sizes.medium
|
||||
? attachment.sizes.medium.url
|
||||
: attachment.url;
|
||||
|
||||
$logoPreview.html('<img src="' + imageUrl + '" alt="Logo email" />').show();
|
||||
$removeLogoBtn.show();
|
||||
});
|
||||
|
||||
// Ouvrir le sélecteur
|
||||
mediaUploader.open();
|
||||
});
|
||||
|
||||
// Bouton pour supprimer le logo
|
||||
$removeLogoBtn.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$logoIdInput.val('');
|
||||
$logoPreview.empty().hide();
|
||||
$removeLogoBtn.hide();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
67
assets/js/datatables.min.js
vendored
Normal file
67
assets/js/datatables.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
45
templates/admin/dashboard.php
Normal file
45
templates/admin/dashboard.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/** @var string $settings_url */
|
||||
/** @var string $logs_url */
|
||||
?>
|
||||
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('ESI Peppol - Passerelle WooCommerce', 'esi_peppol'); ?></h1>
|
||||
|
||||
<p>
|
||||
<?php esc_html_e(
|
||||
'Ce plugin permet d\'envoyer vos documents de facturation WooCommerce vers le réseau Peppol.
|
||||
Utilisez les boutons ci-dessous pour configurer la connexion et consulter l\'historique des échanges.',
|
||||
'esi_peppol'
|
||||
); ?>
|
||||
</p>
|
||||
|
||||
<?php
|
||||
$vat_notice_message = \ESI_PEPPOL\controllers\PEPPOL_Plugin::get_vat_notice_message();
|
||||
if (!empty($vat_notice_message)) :
|
||||
$api_key = (string) get_option('esi_peppol_api_key', '');
|
||||
$password = (string) get_option('esi_peppol_password', '');
|
||||
$notice_class = ($api_key === '' && $password === '') ? 'notice-info' : 'notice-warning';
|
||||
?>
|
||||
<div class="notice <?php echo esc_attr($notice_class); ?>">
|
||||
<p><?php echo esc_html($vat_notice_message); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p>
|
||||
<a href="<?php echo esc_url($settings_url); ?>" class="button button-primary button-hero">
|
||||
<?php esc_html_e('Configuration du connecteur', 'esi_peppol'); ?>
|
||||
</a>
|
||||
|
||||
<a href="<?php echo esc_url($logs_url); ?>" class="button button-secondary button-hero" style="margin-left: 10px;">
|
||||
<?php esc_html_e('Journal des envois Peppol', 'esi_peppol'); ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
98
templates/admin/logs.php
Normal file
98
templates/admin/logs.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/** @var array $rows */
|
||||
?>
|
||||
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Journal des échanges Peppol', 'esi_peppol'); ?></h1>
|
||||
|
||||
<p>
|
||||
<?php esc_html_e('Voici les derniers enregistrements synchronisés avec l\'API ESIPeppol.', 'esi_peppol'); ?>
|
||||
</p>
|
||||
|
||||
<div id="esi-peppol-logs-result"></div>
|
||||
|
||||
<?php if (empty($rows)) : ?>
|
||||
<p><?php esc_html_e('Aucun enregistrement trouvé pour le moment.', 'esi_peppol'); ?></p>
|
||||
<?php else : ?>
|
||||
<table class="widefat fixed striped" id="esi-peppol-logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('ID', 'esi_peppol'); ?></th>
|
||||
<th><?php esc_html_e('Commande', 'esi_peppol'); ?></th>
|
||||
<th><?php esc_html_e('Document ID', 'esi_peppol'); ?></th>
|
||||
<th><?php esc_html_e('Statut', 'esi_peppol'); ?></th>
|
||||
<th><?php esc_html_e('Succès', 'esi_peppol'); ?></th>
|
||||
<th><?php esc_html_e('Date ajout', 'esi_peppol'); ?></th>
|
||||
<th><?php esc_html_e('Dernière mise à jour', 'esi_peppol'); ?></th>
|
||||
<th><?php esc_html_e('Actions', 'esi_peppol'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $row) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html($row->id); ?></td>
|
||||
<td>
|
||||
<?php if ($row->id_order) : ?>
|
||||
<?php
|
||||
$order = wc_get_order($row->id_order);
|
||||
$customer_name = '';
|
||||
if ($order) {
|
||||
$first_name = $order->get_billing_first_name();
|
||||
$last_name = $order->get_billing_last_name();
|
||||
$customer_name = trim($first_name . ' ' . $last_name);
|
||||
}
|
||||
?>
|
||||
<a href="<?php echo esc_url(get_edit_post_link($row->id_order)); ?>" target="_blank">
|
||||
#<?php echo esc_html($row->id_order); ?>
|
||||
<?php if ($customer_name) : ?>
|
||||
- <?php echo esc_html($customer_name); ?>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php else : ?>
|
||||
<?php echo esc_html($row->id_order); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo esc_html($row->document_id); ?></td>
|
||||
<td>
|
||||
<span class="statut statut-<?php echo esc_attr(strtolower(sanitize_html_class($row->status))); ?>">
|
||||
<?php echo esc_html($row->status); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
echo $row->success
|
||||
? '<span style="color:green;">' . esc_html__('Oui', 'esi_peppol') . '</span>'
|
||||
: '<span style="color:red;">' . esc_html__('Non', 'esi_peppol') . '</span>';
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo esc_html($row->date_add); ?></td>
|
||||
<td><?php echo esc_html($row->date_update); ?></td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="button button-secondary esi-peppol-log-resend"
|
||||
data-order-id="<?php echo esc_attr($row->id_order); ?>"
|
||||
>
|
||||
<?php esc_html_e('Renvoyer', 'esi_peppol'); ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button button-secondary esi-peppol-log-status"
|
||||
data-order-id="<?php echo esc_attr($row->id_order); ?>"
|
||||
>
|
||||
<?php esc_html_e('Status', 'esi_peppol'); ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
178
templates/admin/settings.php
Normal file
178
templates/admin/settings.php
Normal file
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Valeurs actuelles en base
|
||||
$api_key = get_option('esi_peppol_api_key', '');
|
||||
$password = get_option('esi_peppol_password', '');
|
||||
$email = get_option('esi_peppol_email', '');
|
||||
$logo_email_id = get_option('esi_peppol_logo_email_id', '');
|
||||
|
||||
// Récupérer l'URL de l'image pour la preview
|
||||
$logo_email_url = '';
|
||||
if ($logo_email_id) {
|
||||
$logo_email_url = wp_get_attachment_image_url((int) $logo_email_id, 'medium');
|
||||
}
|
||||
|
||||
// Messages de feedback (optionnels, passés via $notice / $error)
|
||||
/** @var string|null $notice */
|
||||
/** @var string|null $error */
|
||||
?>
|
||||
|
||||
<div class="wrap esi-peppol-settings-wrap">
|
||||
<h1><?php esc_html_e('Configuration ESI Peppol', 'esi_peppol'); ?></h1>
|
||||
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
wp_kses_post(
|
||||
/* translators: %s: URL de la page de démonstration API */
|
||||
__('Vous pouvez tester et consulter la documentation de l\'API sur la page de démonstration : <a href="%s" target="_blank" rel="noopener noreferrer">ESIPeppol API Demo</a>.', 'esi_peppol')
|
||||
),
|
||||
esc_url('https://scrada.esiweb.pro/api-demo.html')
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
|
||||
<?php
|
||||
$vat_notice_message = \ESI_PEPPOL\controllers\PEPPOL_Plugin::get_vat_notice_message();
|
||||
if (!empty($vat_notice_message)) :
|
||||
// Si les identifiants ESI Peppol sont déjà saisis, on passe en notice "warning"
|
||||
$api_key = (string) get_option('esi_peppol_api_key', '');
|
||||
$password = (string) get_option('esi_peppol_password', '');
|
||||
$notice_class = ($api_key === '' && $password === '') ? 'notice-info' : 'notice-warning';
|
||||
?>
|
||||
<div class="notice <?php echo esc_attr($notice_class); ?>">
|
||||
<p><?php echo esc_html($vat_notice_message); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($notice)) : ?>
|
||||
<div class="notice notice-success is-dismissible">
|
||||
<p><?php echo esc_html($notice); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($error)) : ?>
|
||||
<div class="notice notice-error">
|
||||
<p><?php echo esc_html($error); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="esi-peppol-settings-card">
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('esi_peppol_save_settings', 'esi_peppol_nonce'); ?>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="esi_peppol_api_key"><?php esc_html_e('API Key', 'esi_peppol'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text"
|
||||
class="regular-text"
|
||||
id="esi_peppol_api_key"
|
||||
name="esi_peppol_api_key"
|
||||
value="<?php echo esc_attr($api_key); ?>"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="esi_peppol_password"><?php esc_html_e('Password', 'esi_peppol'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<div class="esi-peppol-password-wrapper">
|
||||
<input type="password"
|
||||
class="regular-text"
|
||||
id="esi_peppol_password"
|
||||
name="esi_peppol_password"
|
||||
value="<?php echo esc_attr($password); ?>"
|
||||
/>
|
||||
<button type="button"
|
||||
class="button button-small esi-peppol-password-toggle"
|
||||
aria-label="<?php esc_attr_e('Afficher/Masquer le mot de passe', 'esi_peppol'); ?>"
|
||||
data-target="#esi_peppol_password">
|
||||
<?php esc_html_e('Afficher', 'esi_peppol'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="esi_peppol_email"><?php esc_html_e('Email', 'esi_peppol'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="email"
|
||||
class="regular-text"
|
||||
id="esi_peppol_email"
|
||||
name="esi_peppol_email"
|
||||
value="<?php echo esc_attr($email); ?>"
|
||||
/>
|
||||
<p class="description">
|
||||
<?php esc_html_e('En cas d\'erreur d\'envoi des factures vous recevrez un email via l\'adresse encodée ci-dessus.', 'esi_peppol'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- <tr>
|
||||
<th scope="row">
|
||||
<label for="esi_peppol_logo_email"><?php esc_html_e('Logo Email', 'esi_peppol'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="hidden"
|
||||
id="esi_peppol_logo_email_id"
|
||||
name="esi_peppol_logo_email_id"
|
||||
value="<?php echo esc_attr($logo_email_id); ?>"
|
||||
/>
|
||||
<button type="button"
|
||||
id="esi-peppol-upload-logo"
|
||||
class="button">
|
||||
<?php esc_html_e('Choisir un logo', 'esi_peppol'); ?>
|
||||
</button>
|
||||
<button type="button"
|
||||
id="esi-peppol-remove-logo"
|
||||
class="button button-link-delete"
|
||||
style="<?php echo $logo_email_id ? '' : 'display:none;'; ?>">
|
||||
<?php esc_html_e('Supprimer', 'esi_peppol'); ?>
|
||||
</button>
|
||||
<div id="esi-peppol-logo-preview" class="esi-peppol-logo-preview" style="<?php echo $logo_email_url ? '' : 'display:none;'; ?>">
|
||||
<?php if ($logo_email_url) : ?>
|
||||
<img src="<?php echo esc_url($logo_email_url); ?>" alt="<?php esc_attr_e('Logo email', 'esi_peppol'); ?>" />
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="description">
|
||||
<?php esc_html_e('Logo affiché dans les emails Peppol.', 'esi_peppol'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr> -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<button type="submit"
|
||||
name="esi_peppol_action"
|
||||
value="save"
|
||||
class="button button-primary">
|
||||
<?php esc_html_e('Enregistrer', 'esi_peppol'); ?>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
id="esi-peppol-test-connection"
|
||||
class="button button-secondary"
|
||||
style="margin-left: 10px;">
|
||||
<?php esc_html_e('Tester la connexion', 'esi_peppol'); ?>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<div id="esi-peppol-test-result"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
201
templates/email/account-activation.blade.php
Normal file
201
templates/email/account-activation.blade.php
Normal file
@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ $locale }}" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<title>{{ __('activation.email_subject', [], $locale) }}</title>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<style>
|
||||
table { border-collapse: collapse; }
|
||||
.button-td, .button-a { transition: none !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
a { text-decoration: none; }
|
||||
@media screen and (max-width: 600px) {
|
||||
.email-container { width: 100% !important; }
|
||||
.mobile-padding { padding: 20px 25px !important; }
|
||||
.header-title { font-size: 22px !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, Helvetica, sans-serif; mso-line-height-rule: exactly;">
|
||||
<!-- Wrapper table -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f4f4f4;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 30px 15px;">
|
||||
|
||||
<!-- Email container -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" class="email-container" style="background-color: #ffffff; max-width: 600px;">
|
||||
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td bgcolor="#0B3C61" style="background-color: #0B3C61; padding: 35px 30px; text-align: center;">
|
||||
<!--[if mso]>
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 20px;">
|
||||
<![endif]-->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 20px;">
|
||||
<img src="{{ config('app.url') }}/images/logo-esi-peppol-light.png" alt="ESI PEPPOL" width="130" style="display: block; max-width: 130px; height: auto; border: 0;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<p style="color: #a8c5db; font-size: 13px; margin: 0 0 8px 0; letter-spacing: 1px; text-transform: uppercase; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_header_subtitle', [], $locale) }}
|
||||
</p>
|
||||
<h1 class="header-title" style="color: #ffffff; font-size: 26px; font-weight: bold; margin: 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
ESI PEPPOL
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 40px 45px; background-color: #ffffff;">
|
||||
|
||||
<!-- Greeting -->
|
||||
<p style="color: #1a1a1a; font-size: 17px; font-weight: bold; margin: 0 0 25px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_greeting', [], $locale) }}
|
||||
</p>
|
||||
|
||||
<!-- Main text -->
|
||||
<p style="color: #333333; font-size: 15px; line-height: 24px; margin: 0 0 18px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
<strong>{{ __('activation.email_line_1', [], $locale) }}</strong>
|
||||
</p>
|
||||
|
||||
<p style="color: #333333; font-size: 15px; line-height: 24px; margin: 0 0 18px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_line_2', [], $locale) }}
|
||||
</p>
|
||||
|
||||
<p style="color: #333333; font-size: 15px; line-height: 24px; margin: 0 0 28px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_line_3', [], $locale) }}
|
||||
</p>
|
||||
|
||||
<!-- BUTTON - Compatible Outlook -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding: 8px 0 28px 0;">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ $url }}" style="height:48px;v-text-anchor:middle;width:220px;" arcsize="10%" strokecolor="#0B3C61" fillcolor="#0B3C61">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;">{{ __('activation.email_action', [], $locale) }} →</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td style="border-radius: 5px; background-color: #0B3C61;">
|
||||
<a href="{{ $url }}" target="_blank" style="display: inline-block; background-color: #0B3C61; color: #ffffff; font-size: 15px; font-weight: bold; text-decoration: none; padding: 14px 35px; border-radius: 5px; font-family: Arial, Helvetica, sans-serif; border: none;">
|
||||
{{ __('activation.email_action', [], $locale) }} →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Info notice -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 15px 18px; background-color: #f8f9fa; border-left: 4px solid #0B3C61;">
|
||||
<p style="color: #666666; font-size: 13px; line-height: 20px; margin: 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_line_4', [], $locale) }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Closing text -->
|
||||
<p style="color: #333333; font-size: 15px; line-height: 24px; margin: 22px 0 22px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_line_5', [], $locale) }}
|
||||
</p>
|
||||
|
||||
<!-- Signature -->
|
||||
<p style="color: #333333; font-size: 15px; margin: 0 0 5px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_closing', [], $locale) }}
|
||||
</p>
|
||||
<p style="color: #0B3C61; font-size: 15px; font-weight: bold; margin: 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_team', [], $locale) }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td bgcolor="#0B3C61" style="background-color: #0B3C61; padding: 28px 45px; text-align: center;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 15px;">
|
||||
<img src="{{ config('app.url') }}/images/logo-esi-peppol-light.png" alt="ESI PEPPOL" width="100" style="display: block; max-width: 100px; height: auto; border: 0;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<p style="color: #a8c5db; font-size: 12px; margin: 0 0 5px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
{{ __('activation.email_footer_contact', [], $locale) }}
|
||||
</p>
|
||||
<p style="color: #ffffff; font-size: 13px; margin: 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
087 27 92 14 - web@esi-informatique.com
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End email container -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End wrapper table -->
|
||||
</body>
|
||||
</html>
|
||||
364
templates/email/api-error.php
Normal file
364
templates/email/api-error.php
Normal file
@ -0,0 +1,364 @@
|
||||
<?php
|
||||
/**
|
||||
* Template email pour les erreurs API Peppol
|
||||
*
|
||||
* Variables disponibles :
|
||||
* @var int $order_id ID de la commande WooCommerce
|
||||
* @var string $order_number Numéro de commande
|
||||
* @var int $http_code Code HTTP de la réponse
|
||||
* @var string $error_message Message d'erreur
|
||||
* @var array $error_data Données supplémentaires de l'erreur
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer l'URL du logo
|
||||
if (defined('ESI_PEPPOL_URL')) {
|
||||
$logo_url = ESI_PEPPOL_URL . 'assets/img/LOGO-ESI-PEPPOL_LIGHT.png';
|
||||
} else {
|
||||
$logo_url = plugin_dir_url(dirname(dirname(__FILE__))) . 'assets/img/LOGO-ESI-PEPPOL_LIGHT.png';
|
||||
}
|
||||
$logo_url = esc_url($logo_url);
|
||||
|
||||
// Récupérer le nom du site
|
||||
$site_name = get_bloginfo('name');
|
||||
$site_url = home_url();
|
||||
|
||||
// Récupérer l'objet commande pour obtenir le total et les informations client
|
||||
$order = wc_get_order($order_id);
|
||||
$order_total = '';
|
||||
$customer_name = '';
|
||||
if ($order instanceof WC_Order) {
|
||||
$order_total = $order->get_formatted_order_total();
|
||||
$first_name = $order->get_billing_first_name();
|
||||
$last_name = $order->get_billing_last_name();
|
||||
if (!empty($first_name) || !empty($last_name)) {
|
||||
$customer_name = trim($first_name . ' ' . $last_name);
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<title><?php esc_html_e('Erreur API Peppol', 'esi_peppol'); ?></title>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<style>
|
||||
table { border-collapse: collapse; }
|
||||
.button-td, .button-a { transition: none !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
a { text-decoration: none; }
|
||||
@media screen and (max-width: 600px) {
|
||||
.email-container { width: 100% !important; }
|
||||
.mobile-padding { padding: 20px 25px !important; }
|
||||
.header-title { font-size: 22px !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, Helvetica, sans-serif; mso-line-height-rule: exactly;">
|
||||
<!-- Wrapper table -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f4f4f4;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 30px 15px;">
|
||||
|
||||
<!-- Email container -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" class="email-container" style="background-color: #ffffff; max-width: 600px;">
|
||||
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td bgcolor="#0B3C61" style="background-color: #0B3C61; padding: 35px 30px; text-align: center;">
|
||||
<!--[if mso]>
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 20px;">
|
||||
<![endif]-->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 20px;">
|
||||
<img src="<?php echo $logo_url; ?>" alt="ESI PEPPOL" width="130" style="display: block; max-width: 130px; height: auto; border: 0;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<p style="color: #a8c5db; font-size: 13px; margin: 0 0 8px 0; letter-spacing: 1px; text-transform: uppercase; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php esc_html_e('Erreur API', 'esi_peppol'); ?>
|
||||
</p>
|
||||
<h1 class="header-title" style="color: #ffffff; font-size: 26px; font-weight: bold; margin: 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
ESI PEPPOL
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 40px 45px; background-color: #ffffff;">
|
||||
|
||||
<!-- Greeting -->
|
||||
<p style="color: #1a1a1a; font-size: 17px; font-weight: bold; margin: 0 0 25px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php esc_html_e('Erreur API Peppol', 'esi_peppol'); ?>
|
||||
</p>
|
||||
|
||||
<!-- Main text -->
|
||||
<p style="color: #333333; font-size: 15px; line-height: 24px; margin: 0 0 18px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: site name */
|
||||
esc_html__('Une erreur s\'est produite lors de l\'envoi d\'une facture vers le réseau Peppol sur %s.', 'esi_peppol'),
|
||||
esc_html($site_name)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
|
||||
<!-- Détails de la commande -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 15px 18px; background-color: #f8f9fa; border-left: 4px solid #0B3C61; margin: 20px 0;">
|
||||
<h2 style="margin: 0 0 10px; color: #333333; font-size: 18px; font-weight: bold; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php esc_html_e('Détails de la commande', 'esi_peppol'); ?>
|
||||
</h2>
|
||||
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 20px; font-family: Arial, Helvetica, sans-serif;">
|
||||
<strong><?php esc_html_e('Numéro de commande:', 'esi_peppol'); ?></strong>
|
||||
<?php echo esc_html($order_number); ?><br>
|
||||
<strong><?php esc_html_e('ID de commande:', 'esi_peppol'); ?></strong>
|
||||
<?php echo esc_html($order_id); ?><br>
|
||||
<?php if (!empty($customer_name)) : ?>
|
||||
<strong><?php esc_html_e('Client:', 'esi_peppol'); ?></strong>
|
||||
<?php echo esc_html($customer_name); ?><br>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($order_total)) : ?>
|
||||
<strong><?php esc_html_e('Total:', 'esi_peppol'); ?></strong>
|
||||
<?php echo $order_total; ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Détails de l'erreur -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 15px 18px; background-color: #f8d7da; border-left: 4px solid #dc3545; margin: 20px 0;">
|
||||
<h2 style="margin: 0 0 10px; color: #721c24; font-size: 18px; font-weight: bold; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php esc_html_e('Détails de l\'erreur', 'esi_peppol'); ?>
|
||||
</h2>
|
||||
<p style="margin: 0 0 10px; color: #721c24; font-size: 14px; line-height: 20px; font-family: Arial, Helvetica, sans-serif;">
|
||||
<strong><?php esc_html_e('Code HTTP:', 'esi_peppol'); ?></strong>
|
||||
<?php echo esc_html($http_code); ?>
|
||||
</p>
|
||||
<?php
|
||||
// Extraire le message d'erreur depuis différentes structures possibles
|
||||
$error_message_to_display = $error_message;
|
||||
|
||||
if (!empty($error_data) && 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'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div style="margin: 15px 0; padding: 15px; background-color: rgba(255, 255, 255, 0.5); border: 2px solid #dc3545; border-radius: 4px;">
|
||||
<p style="margin: 0 0 10px; color: #721c24; font-size: 15px; font-weight: bold; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php esc_html_e('Message d\'erreur:', 'esi_peppol'); ?>
|
||||
</p>
|
||||
<p style="margin: 0; color: #721c24; font-size: 14px; line-height: 22px; font-family: Arial, Helvetica, sans-serif; white-space: pre-wrap; word-wrap: break-word;">
|
||||
<?php echo esc_html($error_message_to_display); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($error_data) && is_array($error_data)) : ?>
|
||||
<?php
|
||||
// Extraire les détails supplémentaires si disponibles
|
||||
$error_code = '';
|
||||
$error_details = [];
|
||||
|
||||
// Structure avec error.code et error.details
|
||||
if (isset($error_data['error'])) {
|
||||
if (isset($error_data['error']['code'])) {
|
||||
$error_code = (string) $error_data['error']['code'];
|
||||
}
|
||||
if (isset($error_data['error']['details']) && is_array($error_data['error']['details'])) {
|
||||
$error_details = $error_data['error']['details'];
|
||||
}
|
||||
}
|
||||
|
||||
// Structure avec details directement
|
||||
if (isset($error_data['details']) && is_array($error_data['details'])) {
|
||||
if (empty($error_details)) {
|
||||
$error_details = $error_data['details'];
|
||||
}
|
||||
}
|
||||
|
||||
$document_id = isset($error_data['details']['document_id']) ? $error_data['details']['document_id'] : (isset($error_data['document_id']) ? $error_data['document_id'] : '');
|
||||
?>
|
||||
|
||||
<?php if (!empty($error_code)) : ?>
|
||||
<p style="margin: 10px 0; color: #721c24; font-size: 14px; line-height: 20px; font-family: Arial, Helvetica, sans-serif;">
|
||||
<strong><?php esc_html_e('Code d\'erreur:', 'esi_peppol'); ?></strong>
|
||||
<?php echo esc_html($error_code); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($document_id)) : ?>
|
||||
<p style="margin: 10px 0; color: #721c24; font-size: 14px; line-height: 20px; font-family: Arial, Helvetica, sans-serif;">
|
||||
<strong><?php esc_html_e('ID du document:', 'esi_peppol'); ?></strong>
|
||||
<?php echo esc_html($document_id); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($error_details) && is_array($error_details)) : ?>
|
||||
<?php
|
||||
// Filtrer les détails pertinents à afficher
|
||||
$relevant_details = [];
|
||||
$excluded_keys = ['document_id', 'validation_error', 'processing_details'];
|
||||
|
||||
foreach ($error_details as $key => $value) {
|
||||
if (!in_array($key, $excluded_keys) && !empty($value)) {
|
||||
$relevant_details[$key] = $value;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if (!empty($relevant_details)) : ?>
|
||||
<div style="margin: 10px 0; padding: 10px; background-color: rgba(255, 255, 255, 0.3); border-radius: 4px;">
|
||||
<p style="margin: 0 0 8px; color: #721c24; font-size: 14px; font-weight: bold; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php esc_html_e('Détails supplémentaires:', 'esi_peppol'); ?>
|
||||
</p>
|
||||
<?php foreach ($relevant_details as $key => $value) : ?>
|
||||
<?php if (is_scalar($value)) : ?>
|
||||
<p style="margin: 0 0 5px; color: #721c24; font-size: 13px; line-height: 18px; font-family: Arial, Helvetica, sans-serif;">
|
||||
<strong><?php echo esc_html(ucfirst(str_replace('_', ' ', $key))); ?>:</strong>
|
||||
<?php echo esc_html($value); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="color: #333333; font-size: 15px; line-height: 24px; margin: 22px 0 22px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php esc_html_e('Veuillez vérifier la configuration de votre connecteur Peppol et consulter les logs pour plus de détails.', 'esi_peppol'); ?>
|
||||
</p>
|
||||
|
||||
<!-- BUTTON - Compatible Outlook -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding: 8px 0 28px 0;">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="<?php echo esc_url(admin_url('admin.php?page=esi-peppol-logs')); ?>" style="height:48px;v-text-anchor:middle;width:220px;" arcsize="10%" strokecolor="#0B3C61" fillcolor="#0B3C61">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;"><?php esc_html_e('Consulter les logs', 'esi_peppol'); ?> →</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td style="border-radius: 5px; background-color: #0B3C61;">
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=esi-peppol-logs')); ?>" target="_blank" style="display: inline-block; background-color: #0B3C61; color: #ffffff; font-size: 15px; font-weight: bold; text-decoration: none; padding: 14px 35px; border-radius: 5px; font-family: Arial, Helvetica, sans-serif; border: none;">
|
||||
<?php esc_html_e('Consulter les logs', 'esi_peppol'); ?> →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td bgcolor="#0B3C61" style="background-color: #0B3C61; padding: 28px 45px; text-align: center;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 15px;">
|
||||
<img src="<?php echo $logo_url; ?>" alt="ESI PEPPOL" width="100" style="display: block; max-width: 100px; height: auto; border: 0;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<p style="color: #a8c5db; font-size: 12px; margin: 0 0 5px 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
<?php esc_html_e('Contact', 'esi_peppol'); ?>
|
||||
</p>
|
||||
<p style="color: #ffffff; font-size: 13px; margin: 0; font-family: Arial, Helvetica, sans-serif;">
|
||||
087 27 92 14 - web@esi-informatique.com
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End email container -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End wrapper table -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user