Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ec9c36292 | |||
| 9ed2c2009e | |||
| feaa43c289 | |||
| 11d81d59f0 | |||
| 33eb49452f | |||
| 16fda3c959 | |||
| 75cd96080a | |||
| e14618aa10 | |||
| 9c56e63820 | |||
| a18536ddc4 | |||
| f6835b4da7 | |||
| e09197485a | |||
| 08c7b02559 | |||
| 73fb3385b9 | |||
| dc6f2ac4d3 | |||
| 2b479eb6a3 | |||
| dc823a4ae0 | |||
| ba2a7ddf95 | |||
| 16665dafb2 | |||
| e8aa2c9494 | |||
| f1fd1fe101 | |||
| 5c49216bb8 | |||
| 70a27f2421 |
@ -35,7 +35,7 @@ return [
|
|||||||
*
|
*
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
'is_test_environment' => true,
|
'is_test_environment' => false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timeout pour les requêtes API (en secondes)
|
* Timeout pour les requêtes API (en secondes)
|
||||||
|
|||||||
@ -509,6 +509,12 @@ class PEPPOL_peppol_controller {
|
|||||||
public static function build_payload_from_order(\WC_Order $order): array {
|
public static function build_payload_from_order(\WC_Order $order): array {
|
||||||
$order_id = $order->get_id();
|
$order_id = $order->get_id();
|
||||||
$order_number = $order->get_order_number();
|
$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();
|
$currency_code = $order->get_currency();
|
||||||
|
|
||||||
$invoice_date = $order->get_date_created()
|
$invoice_date = $order->get_date_created()
|
||||||
@ -549,6 +555,25 @@ class PEPPOL_peppol_controller {
|
|||||||
$order
|
$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
|
// Données vendeur (store) - basées sur les options WooCommerce
|
||||||
$store_name = \get_bloginfo('name');
|
$store_name = \get_bloginfo('name');
|
||||||
$store_address = \get_option('woocommerce_store_address', '');
|
$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_amount_excluding_vat' => round($total_excl_vat, 2),
|
||||||
'total_vat_amount' => round($total_vat, 2),
|
'total_vat_amount' => round($total_vat, 2),
|
||||||
'total_amount_including_vat' => round($total_incl_vat, 2),
|
'total_amount_including_vat' => round($total_incl_vat, 2),
|
||||||
'total_paid_amount' => round($order->get_total(), 2),
|
'total_paid_amount' => 0,
|
||||||
'total_payable_amount' => 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);
|
$invoice_totals = \apply_filters('esi_peppol_invoice_totals', $invoice_totals, $order);
|
||||||
@ -995,12 +1019,13 @@ class PEPPOL_peppol_controller {
|
|||||||
'document_type' => 'invoice',
|
'document_type' => 'invoice',
|
||||||
'external_reference' => $external_reference,
|
'external_reference' => $external_reference,
|
||||||
'invoice' => [
|
'invoice' => [
|
||||||
'invoice_number' => $order_number,
|
'invoice_number' => $invoice_number,
|
||||||
'invoice_date' => $invoice_date,
|
'invoice_date' => $invoice_date,
|
||||||
'due_date' => $due_date,
|
'due_date' => $due_date,
|
||||||
'currency_code' => $currency_code,
|
'currency_code' => $currency_code,
|
||||||
'invoice_notes' => $invoice_notes,
|
'invoice_notes' => $invoice_notes,
|
||||||
'payment_terms' => $payment_terms,
|
'payment_terms' => $payment_terms,
|
||||||
|
'payment_means_code' => $payment_means_code,
|
||||||
],
|
],
|
||||||
'parties' => [
|
'parties' => [
|
||||||
'seller' => $seller,
|
'seller' => $seller,
|
||||||
@ -1014,4 +1039,142 @@ class PEPPOL_peppol_controller {
|
|||||||
|
|
||||||
return \apply_filters('esi_peppol_payload_from_order', $payload, $order);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -155,12 +155,21 @@ class PEPPOL_Plugin {
|
|||||||
$password = isset($_POST['esi_peppol_password']) ? sanitize_text_field(wp_unslash($_POST['esi_peppol_password'])) : '';
|
$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'])) : '';
|
$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;
|
$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
|
// Sauvegarde systématique des options
|
||||||
update_option('esi_peppol_api_key', $api_key);
|
update_option('esi_peppol_api_key', $api_key);
|
||||||
update_option('esi_peppol_password', $password);
|
update_option('esi_peppol_password', $password);
|
||||||
update_option('esi_peppol_email', $email);
|
update_option('esi_peppol_email', $email);
|
||||||
update_option('esi_peppol_logo_email_id', $logo_email_id);
|
update_option('esi_peppol_logo_email_id', $logo_email_id);
|
||||||
|
update_option('esi_peppol_invoice_number_format', $invoice_number_format);
|
||||||
|
|
||||||
if ($action === 'save') {
|
if ($action === 'save') {
|
||||||
$notice = __('Paramètres enregistrés avec succès.', 'esi_peppol');
|
$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.
|
* 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_get_vat_meta_keys().
|
||||||
* Utilise la fonction helper PEPPOL_Woo_Helper::esi_detect_vat_fields_billing_shipping().
|
|
||||||
*
|
*
|
||||||
* @return string|null La clé du champ TVA détecté, ou null si aucun trouvé
|
* @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
|
// 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)
|
// 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);
|
update_option('esi_peppol_vat_field_auto_detected', true);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les dernières commandes pour scanner les champs TVA
|
$vat_keys = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_vat_meta_keys(50);
|
||||||
$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']++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si aucun champ trouvé, marquer que la détection a été effectuée et retourner null
|
// 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);
|
update_option('esi_peppol_vat_field_auto_detected', true);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trier : d'abord par groupe (billing avant shipping), puis par nombre d'occurrences
|
// Prendre la première clé trouvée
|
||||||
usort($vat_fields_found, function ($a, $b) {
|
$vat_field_key = reset($vat_keys);
|
||||||
// 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'];
|
|
||||||
|
|
||||||
// Sauvegarder le champ détecté
|
// Sauvegarder le champ détecté
|
||||||
update_option('esi_peppol_vat_field_key', $vat_field_key);
|
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
|
$vat_keys = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_vat_meta_keys();
|
||||||
$orders = wc_get_orders([
|
$vat_fields_found = array_map(function ($key) {
|
||||||
'limit' => 50,
|
return [
|
||||||
'orderby' => 'date',
|
'key' => $key,
|
||||||
'order' => 'DESC',
|
'count' => 0,
|
||||||
'status' => ['completed', 'processing', 'on-hold'],
|
'sample_value' => null,
|
||||||
]);
|
];
|
||||||
|
}, $vat_keys);
|
||||||
$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'];
|
|
||||||
});
|
|
||||||
|
|
||||||
wp_send_json_success([
|
wp_send_json_success([
|
||||||
'fields' => array_values($vat_fields_found),
|
'fields' => array_values($vat_fields_found),
|
||||||
@ -1184,7 +1053,14 @@ class PEPPOL_Plugin {
|
|||||||
*
|
*
|
||||||
* @return string|null
|
* @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é
|
// Vérifier d'abord si un champ TVA client a été détecté
|
||||||
$vat_field_key = (string) get_option('esi_peppol_vat_field_key', '');
|
$vat_field_key = (string) get_option('esi_peppol_vat_field_key', '');
|
||||||
$auto_detected = (bool) get_option('esi_peppol_vat_field_auto_detected', false);
|
$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
|
// Si aucun champ TVA détecté, proposer la détection dans settings
|
||||||
if ($vat_field_key === '' && $auto_detected) {
|
if ($vat_field_key === '' && $auto_detected) {
|
||||||
$settings_url = admin_url('admin.php?page=esi-peppol-settings');
|
$settings_url = admin_url('admin.php?page=esi-peppol-settings');
|
||||||
return sprintf(
|
$notices[] = [
|
||||||
/* translators: %s: URL vers la page de réglages */
|
'message' => sprintf(
|
||||||
__('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'),
|
/* translators: %s: URL vers la page de réglages */
|
||||||
esc_url($settings_url)
|
__('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', '');
|
$vat_number = (string) get_option('woocommerce_store_vat_number', '');
|
||||||
|
|
||||||
if ($vat_number !== '') {
|
// Si le numéro de TVA est renseigné, on ne vérifie pas les autres notices liées au TVA
|
||||||
return null;
|
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', '');
|
return $notices;
|
||||||
$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'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1237,27 +1123,146 @@ class PEPPOL_Plugin {
|
|||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$api_key = (string) \get_option('esi_peppol_api_key', '');
|
foreach ($vat_notices as $notice) {
|
||||||
$password = (string) \get_option('esi_peppol_password', '');
|
$notice_class = 'notice-' . $notice['type'];
|
||||||
$vat_field_key = (string) \get_option('esi_peppol_vat_field_key', '');
|
echo '<div class="notice ' . \esc_attr($notice_class) . '"><p>' .
|
||||||
$auto_detected = (bool) \get_option('esi_peppol_vat_field_auto_detected', false);
|
\wp_kses_post($notice['message']) .
|
||||||
|
'</p></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Si aucun champ TVA détecté, utiliser notice-warning
|
/**
|
||||||
if ($vat_field_key === '' && $auto_detected) {
|
* Vérifie l'état de configuration des settings ESI Peppol
|
||||||
$notice_class = 'notice-warning';
|
*
|
||||||
} else {
|
* @return array Tableau avec l'état de chaque étape du setup
|
||||||
$notice_class = ($api_key === '' && $password === '') ? 'notice-info' : 'notice-warning';
|
*/
|
||||||
|
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>' .
|
// Vérifier le champ TVA client
|
||||||
\wp_kses_post($vat_notice_message) .
|
$vat_field_key = (string) get_option('esi_peppol_vat_field_key', '');
|
||||||
'</p></div>';
|
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') {
|
public static function write_log($message, $level = 'INFO') {
|
||||||
|
|||||||
@ -46,12 +46,19 @@ class PEPPOL_Woocommerce_controller {
|
|||||||
public static function post_payment(int $order_id): void {
|
public static function post_payment(int $order_id): void {
|
||||||
$order_id = (int) $order_id;
|
$order_id = (int) $order_id;
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
if ($order_id <= 0) {
|
if ($order_id <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$order = \wc_get_order($order_id);
|
$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) {
|
if (!$order instanceof \WC_Order) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -91,6 +98,9 @@ class PEPPOL_Woocommerce_controller {
|
|||||||
return;
|
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
|
// Construire le payload JSON à partir de la commande
|
||||||
$payload = PEPPOL_peppol_controller::build_payload_from_order($order);
|
$payload = PEPPOL_peppol_controller::build_payload_from_order($order);
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
* 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.
|
* 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);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -403,3 +403,169 @@ a.statut:visited {
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
line-height: 1;
|
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: "✓";
|
||||||
|
}
|
||||||
|
|||||||
@ -199,6 +199,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Toggle affichage du mot de passe sur la page de configuration
|
// Toggle affichage du mot de passe sur la page de configuration
|
||||||
|
|
||||||
$('.esi-peppol-password-toggle').on('click', function (e) {
|
$('.esi-peppol-password-toggle').on('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var $btn = $(this);
|
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
|
// Gestion de la détection des champs TVA
|
||||||
var $detectVatBtn = $('#esi-peppol-detect-vat-fields');
|
var $detectVatBtn = $('#esi-peppol-detect-vat-fields');
|
||||||
var $vatFieldsResult = $('#esi-peppol-vat-fields-result');
|
var $vatFieldsResult = $('#esi-peppol-vat-fields-result');
|
||||||
|
|||||||
299
exemple-timeline.html
Normal file
299
exemple-timeline.html
Normal 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>
|
||||||
@ -6,40 +6,160 @@ if (!defined('ABSPATH')) {
|
|||||||
|
|
||||||
/** @var string $settings_url */
|
/** @var string $settings_url */
|
||||||
/** @var string $logs_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>
|
<h1><?php esc_html_e('ESI Peppol - Passerelle WooCommerce', 'esi_peppol'); ?></h1>
|
||||||
|
|
||||||
<p>
|
<p class="esi-peppol-dashboard-intro">
|
||||||
<?php esc_html_e(
|
<?php esc_html_e(
|
||||||
'Ce plugin permet d\'envoyer vos documents de facturation WooCommerce vers le réseau Peppol.
|
'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'
|
'esi_peppol'
|
||||||
); ?>
|
); ?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<?php
|
<!-- Setup guidé - Timeline -->
|
||||||
$vat_notice_message = \ESI_PEPPOL\controllers\PEPPOL_Plugin::get_vat_notice_message();
|
<div class="esi-peppol-setup-guide">
|
||||||
if (!empty($vat_notice_message)) :
|
<h2 style="margin-bottom: 30px;"><?php esc_html_e('Configuration du connecteur', 'esi_peppol'); ?></h2>
|
||||||
$api_key = (string) get_option('esi_peppol_api_key', '');
|
|
||||||
$password = (string) get_option('esi_peppol_password', '');
|
<!-- Étape 1: Numéro TVA boutique -->
|
||||||
$notice_class = ($api_key === '' && $password === '') ? 'notice-info' : 'notice-warning';
|
<div class="step <?php echo $settings_status['vat_number']['completed'] ? 'is-done' : ''; ?>"
|
||||||
?>
|
data-step="<?php echo $settings_status['vat_number']['completed'] ? '✓' : '1'; ?>">
|
||||||
<div class="notice <?php echo esc_attr($notice_class); ?>">
|
<div class="step-card">
|
||||||
<p><?php echo esc_html($vat_notice_message); ?></p>
|
<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>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<p>
|
<!-- Étape 2: Champ TVA client -->
|
||||||
<a href="<?php echo esc_url($settings_url); ?>" class="button button-primary button-hero">
|
<div class="step <?php echo $settings_status['vat_field']['completed'] ? 'is-done' : ''; ?>"
|
||||||
<?php esc_html_e('Configuration du connecteur', 'esi_peppol'); ?>
|
data-step="<?php echo $settings_status['vat_field']['completed'] ? '✓' : '2'; ?>">
|
||||||
</a>
|
<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;">
|
<!-- Étape 3: Format num facture -->
|
||||||
<?php esc_html_e('Journal des envois Peppol', 'esi_peppol'); ?>
|
<div class="step <?php echo $settings_status['invoice_number_format']['completed'] ? 'is-done' : ''; ?>"
|
||||||
</a>
|
data-step="<?php echo $settings_status['invoice_number_format']['completed'] ? '✓' : '3'; ?>">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ if ($logo_email_id) {
|
|||||||
<div class="wrap esi-peppol-settings-wrap">
|
<div class="wrap esi-peppol-settings-wrap">
|
||||||
<h1><?php esc_html_e('Configuration ESI Peppol', 'esi_peppol'); ?></h1>
|
<h1><?php esc_html_e('Configuration ESI Peppol', 'esi_peppol'); ?></h1>
|
||||||
|
|
||||||
<p>
|
<!-- <p>
|
||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
wp_kses_post(
|
wp_kses_post(
|
||||||
@ -34,19 +34,18 @@ if ($logo_email_id) {
|
|||||||
esc_url('https://scrada.esiweb.pro/api-demo.html')
|
esc_url('https://scrada.esiweb.pro/api-demo.html')
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
</p>
|
</p> -->
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$vat_notice_message = \ESI_PEPPOL\controllers\PEPPOL_Plugin::get_vat_notice_message();
|
$vat_notices = \ESI_PEPPOL\controllers\PEPPOL_Plugin::get_vat_notice_message();
|
||||||
if (!empty($vat_notice_message)) :
|
if (!empty($vat_notices)) :
|
||||||
// Si les identifiants ESI Peppol sont déjà saisis, on passe en notice "warning"
|
foreach ($vat_notices as $notice) :
|
||||||
$api_key = (string) get_option('esi_peppol_api_key', '');
|
$notice_class = 'notice-' . $notice['type'];
|
||||||
$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 wp_kses_post($notice['message']); ?></p>
|
||||||
<div class="notice <?php echo esc_attr($notice_class); ?>">
|
</div>
|
||||||
<p><?php echo esc_html($vat_notice_message); ?></p>
|
<?php endforeach; ?>
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty($notice)) : ?>
|
<?php if (!empty($notice)) : ?>
|
||||||
@ -194,6 +193,54 @@ if ($logo_email_id) {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
<!-- <tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="esi_peppol_logo_email"><?php esc_html_e('Logo Email', 'esi_peppol'); ?></label>
|
<label for="esi_peppol_logo_email"><?php esc_html_e('Logo Email', 'esi_peppol'); ?></label>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user