Compare commits

...

23 Commits
master ... main

Author SHA1 Message Date
jps
4ec9c36292 passage en prod 2026-01-26 11:45:29 +01:00
jps
9ed2c2009e Ajustement detect tva 2026-01-26 10:59:25 +01:00
jps
feaa43c289 retest 2026-01-25 15:57:54 +01:00
jps
11d81d59f0 test 2026-01-25 15:51:45 +01:00
jps
33eb49452f test 2026-01-25 15:44:22 +01:00
jps
16fda3c959 protection double facture 2026-01-25 15:34:47 +01:00
jps
75cd96080a corr 2 2026-01-25 15:29:55 +01:00
jps
e14618aa10 correction 2026-01-25 15:21:53 +01:00
jps
9c56e63820 Test 2026-01-23 16:32:40 +01:00
jps
a18536ddc4 Merge branch 'main' of https://gitea.imust.org/jps/EsiPeppol-Woocommerce 2026-01-23 12:24:31 +01:00
jps
f6835b4da7 Ajout check num factu 2026-01-23 12:24:18 +01:00
jps
e09197485a Merge branch 'main' of https://gitea.imust.org/jps/EsiPeppol-Woocommerce 2026-01-16 15:57:35 +01:00
jps
08c7b02559 Merge branch 'main' of https://gitea.imust.org/jps/EsiPeppol-Woocommerce 2026-01-16 15:57:03 +01:00
jps
73fb3385b9 Merge branch 'main' of https://gitea.imust.org/jps/EsiPeppol-Woocommerce 2026-01-16 15:51:09 +01:00
jps
dc6f2ac4d3 ajout helper 2026-01-16 15:49:48 +01:00
jps
2b479eb6a3 Merge branch 'main' of https://gitea.imust.org/jps/EsiPeppol-Woocommerce 2026-01-16 15:48:59 +01:00
jps
dc823a4ae0 Ajout dashboard 2026-01-16 15:48:37 +01:00
jps
ba2a7ddf95 rajout css 2026-01-15 16:54:25 +01:00
jps
16665dafb2 Merge branch 'main' of https://gitea.imust.org/jps/EsiPeppol-Woocommerce 2026-01-15 16:51:32 +01:00
jps
e8aa2c9494 ajout css 2026-01-15 16:40:13 +01:00
jps
f1fd1fe101 changement setup 2026-01-15 16:31:39 +01:00
jps
5c49216bb8 Merge branch 'main' of https://gitea.imust.org/jps/EsiPeppol-Woocommerce 2026-01-15 10:10:25 +01:00
jps
70a27f2421 Ajout settings visuels 2026-01-15 10:10:00 +01:00
10 changed files with 1382 additions and 233 deletions

View File

@ -35,7 +35,7 @@ return [
*
* @var bool
*/
'is_test_environment' => true,
'is_test_environment' => false,
/**
* Timeout pour les requêtes API (en secondes)

View File

@ -509,6 +509,12 @@ class PEPPOL_peppol_controller {
public static function build_payload_from_order(\WC_Order $order): array {
$order_id = $order->get_id();
$order_number = $order->get_order_number();
$invoice_number = $order_number;
$wpo_invoice_number = (string) $order->get_meta('_wcpdf_invoice_number', true);
if ($wpo_invoice_number !== '') {
$invoice_number = $wpo_invoice_number;
}
$currency_code = $order->get_currency();
$invoice_date = $order->get_date_created()
@ -549,6 +555,25 @@ class PEPPOL_peppol_controller {
$order
);
$payment_data = PEPPOL_Woo_Helper::esi_get_order_payment_data($order);
$payment_means_code = $payment_data['payment_means_code'] ?? '30';
if (!empty($payment_data['payment_terms'])) {
$payment_terms = $payment_data['payment_terms'];
}
$payment_means_code = \apply_filters(
'esi_peppol_payment_means_code',
$payment_means_code,
$payment_data,
$order
);
$payment_terms = \apply_filters(
'esi_peppol_payment_terms_from_payment',
$payment_terms,
$payment_data,
$order
);
// Données vendeur (store) - basées sur les options WooCommerce
$store_name = \get_bloginfo('name');
$store_address = \get_option('woocommerce_store_address', '');
@ -979,9 +1004,8 @@ class PEPPOL_peppol_controller {
'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' => round($order->get_total(), 2),
'total_payable_amount' => 0,
'amount_due' => round($order->get_total(), 2),
'total_paid_amount' => 0,
'total_payable_amount' => round($order->get_total(), 2),
];
$invoice_totals = \apply_filters('esi_peppol_invoice_totals', $invoice_totals, $order);
@ -995,12 +1019,13 @@ class PEPPOL_peppol_controller {
'document_type' => 'invoice',
'external_reference' => $external_reference,
'invoice' => [
'invoice_number' => $order_number,
'invoice_number' => $invoice_number,
'invoice_date' => $invoice_date,
'due_date' => $due_date,
'currency_code' => $currency_code,
'invoice_notes' => $invoice_notes,
'payment_terms' => $payment_terms,
'payment_means_code' => $payment_means_code,
],
'parties' => [
'seller' => $seller,
@ -1014,4 +1039,142 @@ class PEPPOL_peppol_controller {
return \apply_filters('esi_peppol_payload_from_order', $payload, $order);
}
/**
* Génère et sauvegarde un numéro WPO si format de secours configuré.
*/
public static function ensure_invoice_number_meta(\WC_Order $order): void {
$wpo_invoice_number = (string) $order->get_meta('_wcpdf_invoice_number', true);
if ($wpo_invoice_number !== '') {
return;
}
$use_backup_format = !\ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_has_wpo_invoice_numbers();
if (!$use_backup_format) {
return;
}
$backup_format = (string) \get_option('esi_peppol_invoice_number_format', '');
if ($backup_format === '') {
$backup_format = (string) \get_option('esi_peppol_vat_number_format', '');
}
if ($backup_format !== '') {
$generated = self::build_invoice_number_from_format($backup_format, $order);
if ($generated !== '') {
$order->update_meta_data('_wcpdf_invoice_number', $generated);
$order->save();
}
return;
}
$fallback = (string) $order->get_order_number();
if ($fallback === '') {
return;
}
$order->update_meta_data('_wcpdf_invoice_number', $fallback);
$order->save();
}
/**
* Construit un numéro de facture depuis un format de secours.
* Placeholders supportés : {YYYY}, {YY}, {MM}, {DD}, {ORDER_ID}, {ORDER_NUMBER}
*/
private static function build_invoice_number_from_format(string $format, \WC_Order $order): string {
$date = $order->get_date_created();
$year = $date ? $date->date('Y') : \gmdate('Y');
$month = $date ? $date->date('m') : \gmdate('m');
$day = $date ? $date->date('d') : \gmdate('d');
$sequence = self::get_next_invoice_sequence($format, $order, $year);
$replacements = [
'{YYYY}' => $year,
'{YY}' => substr($year, -2),
'{MM}' => $month,
'{DD}' => $day,
'{ORDER_ID}' => (string) $order->get_id(),
'{ORDER_NUMBER}' => (string) $order->get_order_number(),
'{number}' => $sequence,
];
$result = strtr($format, $replacements);
return trim($result);
}
private static function get_next_invoice_sequence(string $format, \WC_Order $order, string $current_year): string {
if (!function_exists('wc_get_orders')) {
return '1';
}
$orders = \wc_get_orders([
'limit' => 1,
'orderby' => 'date',
'order' => 'DESC',
'status' => 'any',
'meta_key' => '_wcpdf_invoice_number',
'meta_compare' => 'EXISTS',
'exclude' => [$order->get_id()],
'return' => 'objects',
]);
if (empty($orders)) {
return '1';
}
$last_order = $orders[0];
$last_invoice = (string) $last_order->get_meta('_wcpdf_invoice_number', true);
if ($last_invoice === '') {
return '1';
}
$matches = self::match_invoice_number_format($format, $last_invoice);
if (empty($matches['number'])) {
return '1';
}
$last_number = (int) $matches['number'];
$number_width = strlen((string) $matches['number']);
$next_number = $last_number + 1;
if (strpos($format, '{YYYY}') !== false) {
$last_year = (string) ($matches['year'] ?? '');
if ($last_year !== $current_year) {
$next_number = 1;
}
} elseif (strpos($format, '{YY}') !== false) {
$current_year2 = substr($current_year, -2);
$last_year2 = (string) ($matches['year2'] ?? '');
if ($last_year2 !== $current_year2) {
$next_number = 1;
}
}
$next_str = (string) $next_number;
if ($number_width > 1) {
$next_str = str_pad($next_str, $number_width, '0', STR_PAD_LEFT);
}
return $next_str;
}
private static function match_invoice_number_format(string $format, string $value): array {
$pattern = preg_quote($format, '/');
$pattern = str_replace('\{YYYY\}', '(?P<year>\d{4})', $pattern);
$pattern = str_replace('\{YY\}', '(?P<year2>\d{2})', $pattern);
$pattern = str_replace('\{MM\}', '(?P<month>\d{2})', $pattern);
$pattern = str_replace('\{DD\}', '(?P<day>\d{2})', $pattern);
$pattern = str_replace('\{ORDER_ID\}', '\d+', $pattern);
$pattern = str_replace('\{ORDER_NUMBER\}', '.+?', $pattern);
$pattern = str_replace('\{number\}', '(?P<number>\d+)', $pattern);
$pattern = '/^' . $pattern . '$/';
$matches = [];
if (preg_match($pattern, $value, $matches) !== 1) {
return [];
}
return $matches;
}
}

View File

@ -155,12 +155,21 @@ class PEPPOL_Plugin {
$password = isset($_POST['esi_peppol_password']) ? sanitize_text_field(wp_unslash($_POST['esi_peppol_password'])) : '';
$email = isset($_POST['esi_peppol_email']) ? sanitize_email(wp_unslash($_POST['esi_peppol_email'])) : '';
$logo_email_id = isset($_POST['esi_peppol_logo_email_id']) ? absint(wp_unslash($_POST['esi_peppol_logo_email_id'])) : 0;
$invoice_number_format = get_option('esi_peppol_invoice_number_format', '');
if ($invoice_number_format === '') {
$invoice_number_format = get_option('esi_peppol_vat_number_format', '');
}
if (isset($_POST['esi_peppol_invoice_number_format'])) {
$invoice_number_format = sanitize_text_field(wp_unslash($_POST['esi_peppol_invoice_number_format']));
}
$invoice_number_format = self::normalize_invoice_number_format($invoice_number_format);
// Sauvegarde systématique des options
update_option('esi_peppol_api_key', $api_key);
update_option('esi_peppol_password', $password);
update_option('esi_peppol_email', $email);
update_option('esi_peppol_logo_email_id', $logo_email_id);
update_option('esi_peppol_invoice_number_format', $invoice_number_format);
if ($action === 'save') {
$notice = __('Paramètres enregistrés avec succès.', 'esi_peppol');
@ -699,8 +708,7 @@ class PEPPOL_Plugin {
/**
* Détecte automatiquement les champs TVA lors de l'installation.
* Limite la recherche aux champs billing et shipping dans cet ordre.
* Utilise la fonction helper PEPPOL_Woo_Helper::esi_detect_vat_fields_billing_shipping().
* Utilise la fonction helper PEPPOL_Woo_Helper::esi_get_vat_meta_keys().
*
* @return string|null La clé du champ TVA détecté, ou null si aucun trouvé
*/
@ -712,66 +720,22 @@ class PEPPOL_Plugin {
}
// Vérifier si WooCommerce est disponible
if (!function_exists('wc_get_orders')) {
if (!function_exists('wc_get_order')) {
// Marquer que la détection a été tentée (même si WooCommerce n'est pas disponible)
update_option('esi_peppol_vat_field_auto_detected', true);
return null;
}
// Récupérer les dernières commandes pour scanner les champs TVA
$orders = wc_get_orders([
'limit' => 50,
'orderby' => 'date',
'order' => 'DESC',
'status' => ['completed', 'processing', 'on-hold'],
]);
$vat_fields_found = [];
// Utiliser la fonction helper pour détecter les champs TVA dans chaque commande
foreach ($orders as $order) {
if (!$order instanceof \WC_Order) {
continue;
}
$fields = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_detect_vat_fields_billing_shipping($order);
// Compter les occurrences de chaque champ
foreach ($fields as $field) {
$key = $field['key'];
if (!isset($vat_fields_found[$key])) {
$vat_fields_found[$key] = [
'key' => $key,
'count' => 0,
'group' => $field['group'],
];
}
$vat_fields_found[$key]['count']++;
}
}
$vat_keys = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_vat_meta_keys(50);
// Si aucun champ trouvé, marquer que la détection a été effectuée et retourner null
if (empty($vat_fields_found)) {
if (empty($vat_keys)) {
update_option('esi_peppol_vat_field_auto_detected', true);
return null;
}
// Trier : d'abord par groupe (billing avant shipping), puis par nombre d'occurrences
usort($vat_fields_found, function ($a, $b) {
// Priorité au groupe billing
if ($a['group'] === 'billing' && $b['group'] !== 'billing') {
return -1;
}
if ($a['group'] !== 'billing' && $b['group'] === 'billing') {
return 1;
}
// Si même groupe, trier par nombre d'occurrences
return $b['count'] - $a['count'];
});
// Prendre le premier champ trouvé (priorité billing)
$selected_field = reset($vat_fields_found);
$vat_field_key = $selected_field['key'];
// Prendre la première clé trouvée
$vat_field_key = reset($vat_keys);
// Sauvegarder le champ détecté
update_option('esi_peppol_vat_field_key', $vat_field_key);
@ -806,109 +770,14 @@ class PEPPOL_Plugin {
);
}
// Récupérer les dernières commandes pour scanner les champs TVA
$orders = wc_get_orders([
'limit' => 50,
'orderby' => 'date',
'order' => 'DESC',
'status' => ['completed', 'processing', 'on-hold'],
]);
$vat_fields_found = [];
foreach ($orders as $order) {
if (!$order instanceof \WC_Order) {
continue;
}
// Utiliser la fonction helper pour scanner tous les champs TVA possibles
$vat_candidates = [];
// Scanner les meta keys contenant vat/tva
foreach ($order->get_meta_data() as $meta) {
$k = (string) $meta->key;
if (preg_match('/\b(vat|tva)\b/i', $k) || preg_match('/(vat|tva)/i', $k)) {
$v = $meta->value;
if (is_string($v)) {
$v = trim($v);
}
if (!empty($v)) {
if (!isset($vat_candidates[$k])) {
$vat_candidates[$k] = [
'key' => $k,
'value' => $v,
'count' => 0,
];
}
$vat_candidates[$k]['count']++;
}
}
}
// Scanner les champs de checkout
if (function_exists('WC') && WC()->checkout()) {
$fields = WC()->checkout()->get_checkout_fields();
$patterns = ['vat', 'tva', 'btw', 'ust', 'mwst', 'piva', 'moms', 'mva'];
foreach ($fields as $group_fields) {
foreach ($group_fields as $key => $def) {
$k = strtolower($key);
$found = false;
foreach ($patterns as $p) {
if (strpos($k, $p) !== false) {
$found = true;
break;
}
}
if (!$found) {
$label = strtolower((string) ($def['label'] ?? ''));
foreach ($patterns as $p) {
if (preg_match('/\b' . $p . '\b/i', $label)) {
$found = true;
break;
}
}
}
if ($found) {
foreach ([$key, '_' . $key] as $meta_key) {
$val = $order->get_meta($meta_key, true);
if (!empty($val)) {
if (!isset($vat_candidates[$meta_key])) {
$vat_candidates[$meta_key] = [
'key' => $meta_key,
'value' => trim((string) $val),
'count' => 0,
];
}
$vat_candidates[$meta_key]['count']++;
}
}
}
}
}
}
// Ajouter les candidats trouvés
foreach ($vat_candidates as $candidate) {
$key = $candidate['key'];
if (!isset($vat_fields_found[$key])) {
$vat_fields_found[$key] = [
'key' => $key,
'count' => 0,
'sample_value' => $candidate['value'],
];
}
$vat_fields_found[$key]['count'] += $candidate['count'];
}
}
// Trier par nombre d'occurrences (décroissant)
usort($vat_fields_found, function ($a, $b) {
return $b['count'] - $a['count'];
});
$vat_keys = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_vat_meta_keys();
$vat_fields_found = array_map(function ($key) {
return [
'key' => $key,
'count' => 0,
'sample_value' => null,
];
}, $vat_keys);
wp_send_json_success([
'fields' => array_values($vat_fields_found),
@ -1184,7 +1053,14 @@ class PEPPOL_Plugin {
*
* @return string|null
*/
public static function get_vat_notice_message(): ?string {
/**
* Récupère toutes les notices TVA à afficher
*
* @return array Tableau de notices, chaque notice contient 'message' et 'type' (info, warning, etc.)
*/
public static function get_vat_notice_message(): array {
$notices = [];
// Vérifier d'abord si un champ TVA client a été détecté
$vat_field_key = (string) get_option('esi_peppol_vat_field_key', '');
$auto_detected = (bool) get_option('esi_peppol_vat_field_auto_detected', false);
@ -1192,35 +1068,45 @@ class PEPPOL_Plugin {
// Si aucun champ TVA détecté, proposer la détection dans settings
if ($vat_field_key === '' && $auto_detected) {
$settings_url = admin_url('admin.php?page=esi-peppol-settings');
return sprintf(
/* translators: %s: URL vers la page de réglages */
__('Aucun champ TVA client n\'a été détecté automatiquement. Vous pouvez utiliser l\'outil de détection dans <a href="%s">ESI Peppol > Configuration</a> pour rechercher manuellement les champs TVA dans vos commandes.', 'esi_peppol'),
esc_url($settings_url)
);
$notices[] = [
'message' => sprintf(
/* translators: %s: URL vers la page de réglages */
__('Aucun champ TVA client n\'a été détecté automatiquement. Vous pouvez utiliser l\'outil de détection dans <a href="%s">ESI Peppol > Configuration</a> pour rechercher manuellement les champs TVA dans vos commandes.', 'esi_peppol'),
esc_url($settings_url)
),
'type' => 'warning'
];
}
$vat_number = (string) get_option('woocommerce_store_vat_number', '');
if ($vat_number !== '') {
return null;
// Si le numéro de TVA est renseigné, on ne vérifie pas les autres notices liées au TVA
if ($vat_number === '') {
$api_key = (string) get_option('esi_peppol_api_key', '');
$password = (string) get_option('esi_peppol_password', '');
if ($api_key === '' && $password === '') {
// Invitation initiale
$notices[] = [
'message' => __(
'Pour finaliser la mise en place de Peppol, pensez à renseigner le numéro de TVA de la boutique dans WooCommerce > Réglages > Général.',
'esi_peppol'
),
'type' => 'info'
];
} else {
// Rappel après configuration du connecteur
$notices[] = [
'message' => __(
'Votre connecteur ESI Peppol est configuré, mais le numéro de TVA de la boutique n\'est pas encore renseigné dans WooCommerce > Réglages > Général. Sans ce numéro, les factures Peppol risquent d\'être incomplètes.',
'esi_peppol'
),
'type' => 'warning'
];
}
}
$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'
);
return $notices;
}
/**
@ -1237,27 +1123,146 @@ class PEPPOL_Plugin {
return;
}
$vat_notice_message = self::get_vat_notice_message();
$vat_notices = self::get_vat_notice_message();
if (empty($vat_notice_message)) {
if (empty($vat_notices)) {
return;
}
$api_key = (string) \get_option('esi_peppol_api_key', '');
$password = (string) \get_option('esi_peppol_password', '');
$vat_field_key = (string) \get_option('esi_peppol_vat_field_key', '');
$auto_detected = (bool) \get_option('esi_peppol_vat_field_auto_detected', false);
foreach ($vat_notices as $notice) {
$notice_class = 'notice-' . $notice['type'];
echo '<div class="notice ' . \esc_attr($notice_class) . '"><p>' .
\wp_kses_post($notice['message']) .
'</p></div>';
}
}
// Si aucun champ TVA détecté, utiliser notice-warning
if ($vat_field_key === '' && $auto_detected) {
$notice_class = 'notice-warning';
} else {
$notice_class = ($api_key === '' && $password === '') ? 'notice-info' : 'notice-warning';
/**
* Vérifie l'état de configuration des settings ESI Peppol
*
* @return array Tableau avec l'état de chaque étape du setup
*/
public static function check_settings_status(): array {
$status = [
'vat_number' => [
'completed' => false,
'value' => '',
'label' => __('Numéro de TVA de la boutique', 'esi_peppol'),
'description' => __('Le numéro de TVA de votre boutique doit être renseigné dans WooCommerce > Réglages > Général', 'esi_peppol'),
'action_url' => admin_url('admin.php?page=wc-settings&tab=general'),
'action_label' => __('Configurer dans WooCommerce', 'esi_peppol'),
],
'vat_field' => [
'completed' => false,
'value' => '',
'label' => __('Champ TVA client', 'esi_peppol'),
'description' => __('Le champ utilisé pour récupérer le numéro de TVA du client dans les commandes', 'esi_peppol'),
'action_url' => admin_url('admin.php?page=esi-peppol-settings'),
'action_label' => __('Détecter le champ TVA', 'esi_peppol'),
],
'invoice_number_format' => [
'completed' => false,
'value' => '',
'label' => __('Format num facture', 'esi_peppol'),
'description' => __('Format du numéro de facture (backup si aucun format détecté automatiquement)', 'esi_peppol'),
'action_url' => admin_url('admin.php?page=esi-peppol-settings'),
'action_label' => __('Configurer le format', 'esi_peppol'),
],
'api_credentials' => [
'completed' => false,
'value' => '',
'label' => __('Identifiants API', 'esi_peppol'),
'description' => __('Votre clé API et mot de passe pour vous connecter à la plateforme ESI Peppol', 'esi_peppol'),
'action_url' => admin_url('admin.php?page=esi-peppol-settings'),
'action_label' => __('Configurer les identifiants', 'esi_peppol'),
],
'webhook' => [
'completed' => true, // Toujours disponible
'value' => '',
'label' => __('URL Webhook', 'esi_peppol'),
'description' => __('URL à configurer dans votre profil d\'entreprise sur la plateforme', 'esi_peppol'),
'action_url' => admin_url('admin.php?page=esi-peppol-settings'),
'action_label' => __('Voir l\'URL webhook', 'esi_peppol'),
],
'email' => [
'completed' => false,
'optional' => true,
'value' => '',
'label' => __('Email de notification', 'esi_peppol'),
'description' => __('Adresse email pour recevoir les notifications en cas d\'erreur d\'envoi des factures', 'esi_peppol'),
'action_url' => admin_url('admin.php?page=esi-peppol-settings'),
'action_label' => __('Configurer l\'email', 'esi_peppol'),
],
];
// Vérifier le numéro de TVA de la boutique
$vat_number = (string) get_option('woocommerce_store_vat_number', '');
if ($vat_number !== '') {
$status['vat_number']['completed'] = true;
$status['vat_number']['value'] = $vat_number;
}
echo '<div class="notice ' . \esc_attr($notice_class) . '"><p>' .
\wp_kses_post($vat_notice_message) .
'</p></div>';
// Vérifier le champ TVA client
$vat_field_key = (string) get_option('esi_peppol_vat_field_key', '');
if ($vat_field_key !== '') {
$status['vat_field']['completed'] = true;
$status['vat_field']['value'] = $vat_field_key;
}
// Vérifier le format num TVA (détecté ou backup)
$detected_format = '';
if (\ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_has_wpo_invoice_numbers()) {
$detected_format = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_wpo_invoice_number_format_label();
}
$backup_format = (string) get_option('esi_peppol_invoice_number_format', '');
if ($backup_format === '') {
$backup_format = (string) get_option('esi_peppol_vat_number_format', '');
}
if ($detected_format !== '') {
$status['invoice_number_format']['completed'] = true;
$status['invoice_number_format']['value'] = sprintf(
__('Format détecté : %s', 'esi_peppol'),
$detected_format
);
} elseif ($backup_format !== '') {
$status['invoice_number_format']['completed'] = true;
$status['invoice_number_format']['value'] = $backup_format;
}
// Vérifier les identifiants API
$api_key = (string) get_option('esi_peppol_api_key', '');
$password = (string) get_option('esi_peppol_password', '');
if ($api_key !== '' && $password !== '') {
$status['api_credentials']['completed'] = true;
$status['api_credentials']['value'] = substr($api_key, 0, 8) . '...'; // Masquer partiellement
}
// Récupérer l'URL webhook
if (class_exists(\ESI_PEPPOL\controllers\PEPPOL_Webhook_controller::class)) {
$webhook_url = \ESI_PEPPOL\controllers\PEPPOL_Webhook_controller::get_webhook_url();
$status['webhook']['value'] = $webhook_url;
}
// Vérifier l'email
$email = (string) get_option('esi_peppol_email', '');
if ($email !== '') {
$status['email']['completed'] = true;
$status['email']['value'] = $email;
}
return $status;
}
private static function normalize_invoice_number_format(string $format): string {
$format = trim($format);
if ($format === '') {
return '';
}
$format = str_replace('{number}', '', $format);
$format = trim($format);
return $format . '{number}';
}
public static function write_log($message, $level = 'INFO') {

View File

@ -46,12 +46,19 @@ class PEPPOL_Woocommerce_controller {
public static function post_payment(int $order_id): void {
$order_id = (int) $order_id;
global $wpdb;
if ($order_id <= 0) {
return;
}
$order = \wc_get_order($order_id);
$is_invoice_exists = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}esi_peppol_invoices WHERE order_id = %d", $order_id));
if ($is_invoice_exists > 0) {
return;
}
if (!$order instanceof \WC_Order) {
return;
}
@ -91,6 +98,9 @@ class PEPPOL_Woocommerce_controller {
return;
}
// Générer un numéro de facture de secours si nécessaire
PEPPOL_peppol_controller::ensure_invoice_number_meta($order);
// Construire le payload JSON à partir de la commande
$payload = PEPPOL_peppol_controller::build_payload_from_order($order);

View File

@ -187,6 +187,114 @@ class PEPPOL_Woo_Helper {
];
}
/**
* Retourne les IDs des commandes ayant un meta_key TVA/VAT.
* Compatible HPOS et legacy.
*
* @param int $limit 0 pour aucune limite.
* @param string $order ASC|DESC
* @param array $statuses Statuts Woo (sans préfixe wc-).
* @return int[]
*/
public static function esi_get_order_ids_with_vat_meta(
int $limit = 0,
string $order = 'DESC',
array $statuses = ['completed', 'processing', 'on-hold']
): array {
global $wpdb;
$order = strtoupper($order) === 'ASC' ? 'ASC' : 'DESC';
$like_tva = '%tva%';
$like_vat = '%vat%';
$exclude_key = 'is_vat_exempt';
$statuses = array_map(function ($status) {
$status = (string) $status;
return strpos($status, 'wc-') === 0 ? $status : 'wc-' . $status;
}, $statuses);
$status_placeholders = implode(',', array_fill(0, count($statuses), '%s'));
$is_hpos = class_exists('\Automattic\WooCommerce\Utilities\OrderUtil')
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
if ($is_hpos) {
$orders_table = $wpdb->prefix . 'wc_orders';
$orders_meta_table = $wpdb->prefix . 'wc_orders_meta';
$sql = "
SELECT DISTINCT o.id
FROM {$orders_table} o
INNER JOIN {$orders_meta_table} om ON om.order_id = o.id
WHERE o.type = 'shop_order'
AND (om.meta_key LIKE %s OR om.meta_key LIKE %s)
AND om.meta_key != %s
AND o.status IN ({$status_placeholders})
ORDER BY o.date_created_gmt {$order}
";
} else {
$sql = "
SELECT DISTINCT p.ID
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
WHERE p.post_type = 'shop_order'
AND (pm.meta_key LIKE %s OR pm.meta_key LIKE %s)
AND pm.meta_key != %s
AND p.post_status IN ({$status_placeholders})
ORDER BY p.post_date {$order}
";
}
$params = array_merge([$like_tva, $like_vat, $exclude_key], $statuses);
if ($limit > 0) {
$sql .= ' LIMIT %d';
$params[] = $limit;
}
$prepared_sql = $wpdb->prepare($sql, $params);
$ids = $wpdb->get_col($prepared_sql);
return array_map('intval', $ids);
}
/**
* Retourne les meta_key TVA/VAT distinctes.
* Compatible HPOS et legacy.
*
* @param int $limit
* @return string[]
*/
public static function esi_get_vat_meta_keys( int $limit = 50 ): array {
global $wpdb;
$like_tva = '%tva%';
$like_vat = '%vat%';
$exclude_key = 'is_vat_exempt';
$is_hpos = class_exists('\Automattic\WooCommerce\Utilities\OrderUtil')
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
$meta_table = $is_hpos ? $wpdb->prefix . 'wc_orders_meta' : $wpdb->postmeta;
$sql = "
SELECT DISTINCT meta_key
FROM {$meta_table}
WHERE (meta_key LIKE %s OR meta_key LIKE %s)
AND meta_key <> %s
LIMIT %d
";
$prepared_sql = $wpdb->prepare($sql, $like_tva, $like_vat, $exclude_key, $limit);
$keys = $wpdb->get_col($prepared_sql);
$keys = array_values(array_unique(array_map('strval', $keys)));
$keys = array_values(array_filter($keys, function ($key) {
return $key !== 'is_vat_exempt';
}));
return $keys;
}
/**
* Détecte tous les champs TVA dans une commande, limités aux groupes billing et shipping.
* Retourne un tableau de tous les champs trouvés avec leur groupe et leur valeur.
@ -286,7 +394,198 @@ class PEPPOL_Woo_Helper {
}
}
// Si aucun champ n'a été trouvé dans les groupes billing/shipping,
// chercher dans toutes les meta keys contenant un pattern
if (empty($vat_fields_found)) {
foreach ($order->get_meta_data() as $meta) {
$k = (string) $meta->key;
$k_lower = strtolower($k);
// Vérifier si la clé contient un pattern TVA
$found = false;
foreach ($patterns as $p) {
if (strpos($k_lower, $p) !== false) {
$found = true;
break;
}
}
if ($found) {
$v = $meta->value;
if (is_string($v)) {
$v = trim($v);
}
if (!empty($v)) {
if (!isset($vat_fields_found[$k])) {
$vat_fields_found[$k] = [
'key' => $k,
'value' => $v,
'group' => 'other', // Groupe "other" pour les champs hors billing/shipping
];
}
}
}
}
}
return array_values($vat_fields_found);
}
/**
* Retourne les infos de paiement utiles pour Peppol.
*
* @param \WC_Order|int $order
* @return array{payment_method:string,payment_method_title:string,payment_means_code:string,payment_terms:string}
*/
public static function esi_get_order_payment_data( $order ): array {
if ( is_numeric( $order ) ) {
$order = wc_get_order( $order );
}
if ( ! $order instanceof \WC_Order ) {
return [
'payment_method' => '',
'payment_method_title' => '',
'payment_means_code' => '30',
'payment_terms' => '',
];
}
$payment_method = (string) $order->get_payment_method();
$payment_method_title = (string) $order->get_payment_method_title();
// Valeur par defaut : virement (code 30)
$payment_means_code = '30';
$map = [
'bacs' => '30', // virement bancaire
'cheque' => '20',
'cod' => '10', // cash on delivery
];
if ( isset( $map[ $payment_method ] ) ) {
$payment_means_code = $map[ $payment_method ];
}
$data = [
'payment_method' => $payment_method,
'payment_method_title' => $payment_method_title,
'payment_means_code' => $payment_means_code,
'payment_terms' => '',
];
return apply_filters( 'esi_peppol_order_payment_data', $data, $order );
}
public static function esi_is_wpo_wcpdf_installed(): bool {
if (!function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins(); // [ 'dir/main.php' => headers... ]
foreach ($plugins as $file => $data) {
if ($file === 'woocommerce-pdf-invoices-packing-slips/woocommerce-pdf-invoices-packingslips.php') {
return true;
}
// Optionnel : fallback par Name (plus fragile, dépend de la traduction / headers)
if (!empty($data['Name']) && stripos($data['Name'], 'PDF Invoices') !== false) {
// pas ultra fiable, mais utile en dernier recours
}
}
return false;
}
/**
* detecte les num de facture enregistré
*/
//wpo_wcpdf_documents_settings_invoice
/*
Array
(
[enabled] => 1
[attach_to_email_ids] => Array
(
[customer_on_hold_order] => 1
[customer_processing_order] => 1
[customer_completed_order] => 1
)
[my_account_buttons] => available
[display_email] => 1
[display_customer_notes] => 1
[display_shipping_address] =>
[display_number] => invoice_number
[number_format] => Array
(
[prefix] => 2026-
[suffix] =>
[padding] =>
)
[display_date] =>
)*/
/**
* Vérifie la présence de numéros de facture WPO WCPDF.
* - Plugin installé
* - Paramètres facture activés
* - Au moins une commande avec meta _wcpdf_invoice_number
*/
public static function esi_has_wpo_invoice_numbers(): bool {
if ( ! self::esi_is_wpo_wcpdf_installed() ) {
return false;
}
if ( ! function_exists( 'wc_get_orders' ) ) {
return false;
}
$settings = get_option( 'wpo_wcpdf_documents_settings_invoice', [] );
if ( empty( $settings ) || ! is_array( $settings ) ) {
return false;
}
if ( empty( $settings['enabled'] ) ) {
return false;
}
if ( empty( $settings['display_number'] ) || $settings['display_number'] !== 'invoice_number' ) {
return false;
}
$orders = wc_get_orders( [
'limit' => 1,
'status' => 'any',
'meta_key' => '_wcpdf_invoice_number',
'meta_compare' => 'EXISTS',
'return' => 'ids',
] );
return ! empty( $orders );
}
/**
* Retourne un libellé lisible du format des numéros WPO WCPDF.
*/
public static function esi_get_wpo_invoice_number_format_label(): string {
$settings = get_option( 'wpo_wcpdf_documents_settings_invoice', [] );
if ( empty( $settings ) || ! is_array( $settings ) ) {
return '';
}
$number_format = $settings['number_format'] ?? [];
if ( ! is_array( $number_format ) ) {
return '';
}
$prefix = (string) ( $number_format['prefix'] ?? '' );
$suffix = (string) ( $number_format['suffix'] ?? '' );
$padding = (string) ( $number_format['padding'] ?? '' );
$format = $prefix . '{number}' . $suffix;
if ( $padding !== '' ) {
$format .= ' (padding: ' . $padding . ')';
}
return $format;
}
}

View File

@ -403,3 +403,169 @@ a.statut:visited {
height: 16px;
line-height: 1;
}
/* ====================================
TIMELINE VERTICALE - DASHBOARD
==================================== */
.esi-peppol-setup-guide {
max-width: 900px;
margin-top: 30px;
}
.step {
position: relative;
padding-left: 70px;
padding-bottom: 40px;
min-height: 80px;
}
/* Dernier élément sans padding-bottom */
.step:last-child {
padding-bottom: 0;
}
/* Cercle numéroté (::before) */
.step::before {
content: attr(data-step);
position: absolute;
left: 0;
top: 0;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 18px;
z-index: 2;
/* État par défaut : cercle blanc, bord gris, numéro gris */
background-color: #ffffff;
border: 3px solid #d1d5db;
color: #9ca3af;
}
/* Ligne verticale (::after) */
.step::after {
content: '';
position: absolute;
left: 22px; /* Centre du cercle (44px / 2) */
top: 44px; /* Sous le cercle */
bottom: 0;
width: 3px;
/* État par défaut : ligne grise */
background-color: #d1d5db;
z-index: 1;
}
/* Masquer la ligne sur le dernier élément */
.step:last-child::after {
display: none;
}
/* Carte de contenu */
.step-card {
background-color: #f3f4f6;
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 20px 24px;
transition: all 0.3s ease;
}
.step-card h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #374151;
}
.step-card p {
margin: 0 0 16px 0;
color: #6b7280;
line-height: 1.6;
}
.step-card .step-value {
display: inline-block;
background-color: #ffffff;
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 8px 12px;
margin-top: 8px;
}
.step-card .step-value strong {
display: block;
margin-bottom: 4px;
font-size: 13px;
color: #6b7280;
}
.step-card .step-value code {
color: #1f2937;
font-size: 14px;
font-weight: 500;
}
.step-card .step-note {
margin-top: 12px;
font-size: 13px;
color: #6b7280;
font-style: italic;
}
/** ÉTAT COMPLÉTÉ : .is-done
======================================== */
.step.is-done::before {
/* Check ✓ ou numéro en bleu */
background-color: #dbeafe;
border-color: #3b82f6;
color: #2563eb;
font-weight: 700;
}
.step.is-done::after {
/* Ligne bleue */
background-color: #3b82f6;
}
.step.is-done .step-card {
/* Carte fond bleu, texte blanc */
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-color: #2563eb;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
}
.step.is-done .step-card h3 {
color: #ffffff;
}
.step.is-done .step-card p {
color: #dbeafe;
}
.step.is-done .step-card .step-value {
background-color: rgba(255, 255, 255, 0.95);
border-color: #2563eb;
}
.step.is-done .step-card .step-value strong {
color: #2563eb;
}
.step.is-done .step-card .step-value code {
color: #1e40af;
background-color: #eff6ff;
padding: 2px 6px;
border-radius: 3px;
}
/* Pour afficher un check ✓ sur les étapes complétées */
.step.is-done[data-step="✓"]::before {
font-size: 22px;
content: "✓";
}

View File

@ -199,6 +199,7 @@
}
// Toggle affichage du mot de passe sur la page de configuration
$('.esi-peppol-password-toggle').on('click', function (e) {
e.preventDefault();
var $btn = $(this);
@ -507,6 +508,45 @@
});
}
// Copier l'URL webhook depuis le dashboard (avec data-copy-text)
$(document).on('click', '.esi-peppol-copy-webhook-url[data-copy-text]', function (e) {
e.preventDefault();
var $btn = $(this);
var text = $btn.attr('data-copy-text');
if (text) {
// Utiliser l'API Clipboard moderne si disponible
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
var originalText = $btn.text();
$btn.text('Copié !');
setTimeout(function () {
$btn.text(originalText);
}, 2000);
}).catch(function (err) {
console.error('Erreur lors de la copie:', err);
});
} else {
// Fallback pour les anciens navigateurs
try {
var $temp = $('<textarea>');
$('body').append($temp);
$temp.val(text).select();
document.execCommand('copy');
$temp.remove();
var originalText = $btn.text();
$btn.text('Copié !');
setTimeout(function () {
$btn.text(originalText);
}, 2000);
} catch (err) {
console.error('Erreur lors de la copie:', err);
}
}
}
});
// Gestion de la détection des champs TVA
var $detectVatBtn = $('#esi-peppol-detect-vat-fields');
var $vatFieldsResult = $('#esi-peppol-vat-fields-result');

299
exemple-timeline.html Normal file
View File

@ -0,0 +1,299 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exemple Timeline Verticale</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background: #ffffff;
border-radius: 16px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
h1 {
text-align: center;
color: #1f2937;
margin-bottom: 40px;
font-size: 32px;
}
/* ====================================
TIMELINE VERTICALE
==================================== */
.timeline {
max-width: 800px;
margin: 0 auto;
}
.step {
position: relative;
padding-left: 70px;
padding-bottom: 40px;
min-height: 80px;
}
/* Dernier élément sans padding-bottom */
.step:last-child {
padding-bottom: 0;
}
/* Cercle numéroté (::before) */
.step::before {
content: attr(data-step);
position: absolute;
left: 0;
top: 0;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 18px;
z-index: 2;
/* État par défaut : cercle blanc, bord gris, numéro gris */
background-color: #ffffff;
border: 3px solid #d1d5db;
color: #9ca3af;
}
/* Ligne verticale (::after) */
.step::after {
content: '';
position: absolute;
left: 22px; /* Centre du cercle (44px / 2) */
top: 44px; /* Sous le cercle */
bottom: 0;
width: 3px;
/* État par défaut : ligne grise */
background-color: #d1d5db;
z-index: 1;
}
/* Masquer la ligne sur le dernier élément */
.step:last-child::after {
display: none;
}
/* Carte de contenu */
.step-card {
background-color: #f3f4f6;
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 20px 24px;
transition: all 0.3s ease;
}
.step-card h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #374151;
}
.step-card p {
margin: 0 0 16px 0;
color: #6b7280;
line-height: 1.6;
}
.step-card .step-value {
display: inline-block;
background-color: #ffffff;
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 8px 12px;
margin-top: 8px;
}
.step-card .step-value strong {
display: block;
margin-bottom: 4px;
font-size: 13px;
color: #6b7280;
}
.step-card .step-value code {
color: #1f2937;
font-size: 14px;
font-weight: 500;
font-family: 'Courier New', Courier, monospace;
}
.step-card .btn {
display: inline-block;
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.step-card .btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* ========================================
ÉTAT COMPLÉTÉ : .is-done
======================================== */
.step.is-done::before {
/* Check ✓ ou numéro en bleu */
background-color: #dbeafe;
border-color: #3b82f6;
color: #2563eb;
font-weight: 700;
}
.step.is-done::after {
/* Ligne bleue */
background-color: #3b82f6;
}
.step.is-done .step-card {
/* Carte fond bleu, texte blanc */
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-color: #2563eb;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
}
.step.is-done .step-card h3 {
color: #ffffff;
}
.step.is-done .step-card p {
color: #dbeafe;
}
.step.is-done .step-card .step-value {
background-color: rgba(255, 255, 255, 0.95);
border-color: #2563eb;
}
.step.is-done .step-card .step-value strong {
color: #2563eb;
}
.step.is-done .step-card .step-value code {
color: #1e40af;
background-color: #eff6ff;
padding: 2px 6px;
border-radius: 3px;
}
/* Pour afficher un check ✓ sur les étapes complétées */
.step.is-done[data-step="✓"]::before {
font-size: 22px;
content: "✓";
}
.note {
text-align: center;
margin-top: 40px;
padding-top: 30px;
border-top: 2px solid #e5e7eb;
color: #6b7280;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>Timeline Verticale - Exemple</h1>
<div class="timeline">
<!-- Étape 1: Complétée -->
<div class="step is-done" data-step="✓">
<div class="step-card">
<h3>Configuration du connecteur</h3>
<p>Le numéro de TVA de votre boutique doit être renseigné dans WooCommerce > Réglages > Général</p>
<div class="step-value">
<strong>Valeur configurée :</strong>
<code>BE0431.066.713</code>
</div>
</div>
</div>
<!-- Étape 2: Complétée -->
<div class="step is-done" data-step="✓">
<div class="step-card">
<h3>Numéro de TVA de la boutique</h3>
<p>Le champ utilisé pour récupérer le numéro de TVA du client dans les commandes</p>
<div class="step-value">
<strong>Champ détecté :</strong>
<code>billing_vat_number</code>
</div>
</div>
</div>
<!-- Étape 3: Non complétée -->
<div class="step" data-step="3">
<div class="step-card">
<h3>Champ TVA client</h3>
<p>Le champ utilisé pour récupérer le numéro de TVA du client dans les commandes</p>
<p style="margin-bottom: 0; color: #ef4444; font-weight: 500;">Aucun champ détecté</p>
</div>
</div>
<!-- Étape 4: Non complétée -->
<div class="step" data-step="4">
<div class="step-card">
<h3>Identifiants API</h3>
<p>Votre clé API et mot de passe pour vous connecter à la plateforme ESI Peppol</p>
<p style="margin-bottom: 0; color: #ef4444; font-weight: 500;">Aucune clé API détectée</p>
</div>
</div>
<!-- Étape 5: Non complétée -->
<div class="step" data-step="5">
<div class="step-card">
<h3>URL Webhook</h3>
<p>URL à configurer dans votre profil d'entreprise sur la plateforme</p>
<div class="step-value">
<strong>Configurez cette URL :</strong>
<code>https://peppol.esi-web.be/webhook/callback</code>
</div>
</div>
</div>
<!-- Étape 6: Non complétée (optionnelle) -->
<div class="step" data-step="6">
<div class="step-card">
<h3>Email de notification (optionnel)</h3>
<p>Adresse email pour recevoir les notifications en cas d'erreur d'envoi des factures</p>
<a href="#" class="btn">Configurer un email</a>
</div>
</div>
</div>
<div class="note">
<p><strong>Structure CSS:</strong> .step avec ::before (cercle) et ::after (ligne)</p>
<p><strong>États:</strong> Par défaut (gris) | .is-done (bleu avec check ✓)</p>
</div>
</div>
</body>
</html>

View File

@ -6,40 +6,160 @@ if (!defined('ABSPATH')) {
/** @var string $settings_url */
/** @var string $logs_url */
// Récupérer l'état des settings
$settings_status = \ESI_PEPPOL\controllers\PEPPOL_Plugin::check_settings_status();
?>
<div class="wrap">
<div class="wrap esi-peppol-dashboard-wrap">
<h1><?php esc_html_e('ESI Peppol - Passerelle WooCommerce', 'esi_peppol'); ?></h1>
<p>
<p class="esi-peppol-dashboard-intro">
<?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.',
Suivez les étapes ci-dessous pour configurer votre connexion.',
'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>
<!-- Setup guidé - Timeline -->
<div class="esi-peppol-setup-guide">
<h2 style="margin-bottom: 30px;"><?php esc_html_e('Configuration du connecteur', 'esi_peppol'); ?></h2>
<!-- Étape 1: Numéro TVA boutique -->
<div class="step <?php echo $settings_status['vat_number']['completed'] ? 'is-done' : ''; ?>"
data-step="<?php echo $settings_status['vat_number']['completed'] ? '✓' : '1'; ?>">
<div class="step-card">
<h3><?php echo esc_html($settings_status['vat_number']['label']); ?></h3>
<p><?php echo esc_html($settings_status['vat_number']['description']); ?></p>
<?php if ($settings_status['vat_number']['completed']) : ?>
<div class="step-value">
<strong><?php esc_html_e('Valeur configurée :', 'esi_peppol'); ?></strong>
<code><?php echo esc_html($settings_status['vat_number']['value']); ?></code>
</div>
<?php else : ?>
<a href="<?php echo esc_url($settings_status['vat_number']['action_url']); ?>" class="button button-primary">
<?php echo esc_html($settings_status['vat_number']['action_label']); ?>
</a>
<?php endif; ?>
</div>
</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>
<!-- Étape 2: Champ TVA client -->
<div class="step <?php echo $settings_status['vat_field']['completed'] ? 'is-done' : ''; ?>"
data-step="<?php echo $settings_status['vat_field']['completed'] ? '✓' : '2'; ?>">
<div class="step-card">
<h3><?php echo esc_html($settings_status['vat_field']['label']); ?></h3>
<p><?php echo esc_html($settings_status['vat_field']['description']); ?></p>
<?php if ($settings_status['vat_field']['completed']) : ?>
<div class="step-value">
<strong><?php esc_html_e('Champ détecté :', 'esi_peppol'); ?></strong>
<code><?php echo esc_html($settings_status['vat_field']['value']); ?></code>
</div>
<?php else : ?>
<a href="<?php echo esc_url($settings_status['vat_field']['action_url']); ?>" class="button button-primary">
<?php echo esc_html($settings_status['vat_field']['action_label']); ?>
</a>
<?php endif; ?>
</div>
</div>
<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>
<!-- Étape 3: Format num facture -->
<div class="step <?php echo $settings_status['invoice_number_format']['completed'] ? 'is-done' : ''; ?>"
data-step="<?php echo $settings_status['invoice_number_format']['completed'] ? '✓' : '3'; ?>">
<div class="step-card">
<h3><?php echo esc_html($settings_status['invoice_number_format']['label']); ?></h3>
<p><?php echo esc_html($settings_status['invoice_number_format']['description']); ?></p>
<?php if ($settings_status['invoice_number_format']['completed']) : ?>
<div class="step-value">
<strong><?php esc_html_e('Valeur :', 'esi_peppol'); ?></strong>
<code><?php echo esc_html($settings_status['invoice_number_format']['value']); ?></code>
</div>
<?php else : ?>
<a href="<?php echo esc_url($settings_status['invoice_number_format']['action_url']); ?>" class="button button-primary">
<?php echo esc_html($settings_status['invoice_number_format']['action_label']); ?>
</a>
<?php endif; ?>
</div>
</div>
<!-- Étape 4: Identifiants API -->
<div class="step <?php echo $settings_status['api_credentials']['completed'] ? 'is-done' : ''; ?>"
data-step="<?php echo $settings_status['api_credentials']['completed'] ? '✓' : '4'; ?>">
<div class="step-card">
<h3><?php echo esc_html($settings_status['api_credentials']['label']); ?></h3>
<p><?php echo esc_html($settings_status['api_credentials']['description']); ?></p>
<?php if ($settings_status['api_credentials']['completed']) : ?>
<div class="step-value">
<strong><?php esc_html_e('Clé API configurée :', 'esi_peppol'); ?></strong>
<code><?php echo esc_html($settings_status['api_credentials']['value']); ?></code>
</div>
<?php else : ?>
<a href="<?php echo esc_url($settings_status['api_credentials']['action_url']); ?>" class="button button-primary">
<?php echo esc_html($settings_status['api_credentials']['action_label']); ?>
</a>
<?php endif; ?>
</div>
</div>
<!-- Étape 5: Webhook -->
<div class="step is-done" data-step="5">
<div class="step-card">
<h3><?php echo esc_html($settings_status['webhook']['label']); ?></h3>
<p><?php echo esc_html($settings_status['webhook']['description']); ?></p>
<div class="esi-peppol-webhook-display">
<code class="esi-peppol-webhook-url"><?php echo esc_html($settings_status['webhook']['value']); ?></code>
<button type="button" class="button button-small esi-peppol-copy-webhook-url" data-copy-text="<?php echo esc_attr($settings_status['webhook']['value']); ?>">
<?php esc_html_e('Copier', 'esi_peppol'); ?>
</button>
</div>
<p class="step-note">
<?php
printf(
wp_kses_post(
/* translators: %s: URL de la documentation */
__('Configurez cette URL dans votre profil d\'entreprise sur <a href="%s" target="_blank" rel="noopener noreferrer">https://peppol.esi-web.be/</a>', 'esi_peppol')
),
esc_url('https://peppol.esi-web.be/')
);
?>
</p>
</div>
</div>
<!-- Étape 6: Email (optionnel) -->
<div class="step <?php echo $settings_status['email']['completed'] ? 'is-done' : ''; ?>"
data-step="<?php echo $settings_status['email']['completed'] ? '✓' : '6'; ?>">
<div class="step-card">
<h3>
<?php echo esc_html($settings_status['email']['label']); ?>
<span style="font-size: 12px; font-weight: normal; opacity: 0.8;">(<?php esc_html_e('Optionnel', 'esi_peppol'); ?>)</span>
</h3>
<p><?php echo esc_html($settings_status['email']['description']); ?></p>
<?php if ($settings_status['email']['completed']) : ?>
<div class="step-value">
<strong><?php esc_html_e('Email configuré :', 'esi_peppol'); ?></strong>
<code><?php echo esc_html($settings_status['email']['value']); ?></code>
</div>
<?php else : ?>
<a href="<?php echo esc_url($settings_status['email']['action_url']); ?>" class="button button-secondary">
<?php echo esc_html($settings_status['email']['action_label']); ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
<!-- Actions rapides -->
<div class="esi-peppol-dashboard-actions">
<h2><?php esc_html_e('Actions rapides', 'esi_peppol'); ?></h2>
<p>
<a href="<?php echo esc_url($settings_url); ?>" class="button button-primary button-hero">
<?php esc_html_e('Configuration avancée', '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>
</div>

View File

@ -24,7 +24,7 @@ if ($logo_email_id) {
<div class="wrap esi-peppol-settings-wrap">
<h1><?php esc_html_e('Configuration ESI Peppol', 'esi_peppol'); ?></h1>
<p>
<!-- <p>
<?php
printf(
wp_kses_post(
@ -34,19 +34,18 @@ if ($logo_email_id) {
esc_url('https://scrada.esiweb.pro/api-demo.html')
);
?>
</p>
</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>
$vat_notices = \ESI_PEPPOL\controllers\PEPPOL_Plugin::get_vat_notice_message();
if (!empty($vat_notices)) :
foreach ($vat_notices as $notice) :
$notice_class = 'notice-' . $notice['type'];
?>
<div class="notice <?php echo esc_attr($notice_class); ?>">
<p><?php echo wp_kses_post($notice['message']); ?></p>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php if (!empty($notice)) : ?>
@ -194,6 +193,54 @@ if ($logo_email_id) {
</td>
</tr>
<tr>
<th scope="row">
<label for="esi_peppol_invoice_number_format"><?php esc_html_e('Format num facture', 'esi_peppol'); ?></label>
</th>
<td>
<?php
$backup_invoice_format = (string) get_option('esi_peppol_invoice_number_format', '');
if ($backup_invoice_format === '') {
$backup_invoice_format = (string) get_option('esi_peppol_vat_number_format', '');
}
$has_wpo_invoices = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_has_wpo_invoice_numbers();
$detected_format = $has_wpo_invoices
? \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_wpo_invoice_number_format_label()
: '';
?>
<?php if ($detected_format !== '') : ?>
<p style="margin: 0;">
<strong><?php esc_html_e('Format détecté :', 'esi_peppol'); ?></strong>
<code style="margin-left: 5px; padding: 2px 6px; background-color: #fff; border: 1px solid #c3c4c7;">
<?php echo esc_html($detected_format); ?>
</code>
</p>
<p class="description">
<?php esc_html_e('Le format est détecté automatiquement via WPO WCPDF. Le champ de backup n\'est pas requis.', 'esi_peppol'); ?>
</p>
<?php else : ?>
<input type="text"
class="regular-text esi-peppol-invoice-format-input"
id="esi_peppol_invoice_number_format"
name="esi_peppol_invoice_number_format"
value="<?php echo esc_attr($backup_invoice_format); ?>"
/>
<div class="esi-peppol-placeholder-buttons" style="margin-top: 8px;">
<button type="button" class="button button-secondary esi-peppol-placeholder-btn" data-placeholder="{YYYY}">{YYYY}</button>
<button type="button" class="button button-secondary esi-peppol-placeholder-btn" data-placeholder="{YY}">{YY}</button>
<button type="button" class="button button-secondary esi-peppol-placeholder-btn" data-placeholder="{MM}">{MM}</button>
<button type="button" class="button button-secondary esi-peppol-placeholder-btn" data-placeholder="{DD}">{DD}</button>
<button type="button" class="button button-secondary esi-peppol-placeholder-btn" data-placeholder="{ORDER_ID}">{ORDER_ID}</button>
<button type="button" class="button button-secondary esi-peppol-placeholder-btn" data-placeholder="{ORDER_NUMBER}">{ORDER_NUMBER}</button>
<button type="button" class="button button-secondary esi-peppol-placeholder-btn" data-placeholder="{number}">{number}</button>
</div>
<p class="description">
<?php esc_html_e('Format de secours si aucun format n\'est détecté automatiquement. Doit se terminer par {number}. Placeholders : {YYYY}, {YY}, {MM}, {DD}, {ORDER_ID}, {ORDER_NUMBER}, {number}.', 'esi_peppol'); ?>
</p>
<?php endif; ?>
</td>
</tr>
<!-- <tr>
<th scope="row">
<label for="esi_peppol_logo_email"><?php esc_html_e('Logo Email', 'esi_peppol'); ?></label>