Importador de tarifas para el plugin «Correos Oficial»

Autor: Luis Muñoz
php code
<?php /** * Importador de tarifas para "Correos Oficial" * - Modo Reemplazar (borra reglas del método seleccionado) * - Modo Añadir (agrega al final sin borrar) * - Inserción robusta (SQL preparado + backticks en `from`/`to`) y reporte de errores * Autor: Luis Muñoz + GPT */ add_action('admin_menu', function () { add_submenu_page( 'woocommerce', 'Importar tarifas Correos', 'Correos → Importar tarifas', 'manage_woocommerce', 'correos-import-rates', 'co_import_rates_admin_page' ); }); function co_import_rates_admin_page() { if ( ! class_exists('WC_Shipping_Zones') ) { echo '<div class="notice notice-error"><p>WooCommerce no está activo.</p></div>'; return; } // Procesar envío if ( isset($_POST['co_import_submit']) && check_admin_referer('co_import_rates') ) { co_handle_import(); } // Zonas (incluye "resto del mundo") $zones = WC_Shipping_Zones::get_zones(); $zones[] = array('zone_id' => 0, 'zone_name' => 'Ubicaciones no cubiertas (Resto del mundo)'); // Zona -> métodos (mostramos todos; podrás elegir el de Correos) $zone_methods = []; foreach ($zones as $z) { $zone = new WC_Shipping_Zone( $z['zone_id'] ); $methods = $zone->get_shipping_methods(); foreach ($methods as $m) { $zone_methods[$z['zone_id']][] = [ 'instance_id' => $m->instance_id, 'method_id' => $m->id, 'label' => $m->get_method_title() . ' ['.$m->id.'] (Instancia: ' . $m->instance_id . ')', ]; } } echo '<div class="wrap"><h1>Importar tarifas Correos</h1>'; echo '<p>CSV con columnas: <code>from,to,cost</code> (opcional: <code>condition,class</code>). '; echo 'Por defecto: <code>condition=weightkg</code>, <code>class=allproducts</code>. Decimales con <strong>punto</strong>.</p>'; echo '<form method="post" enctype="multipart/form-data">'; wp_nonce_field('co_import_rates'); // Zona echo '<p><label><strong>Zona:</strong> '; echo '<select name="zone_id" required>'; foreach ($zones as $z) { $zid = $z['zone_id']; $zname = isset($z['zone_name']) ? $z['zone_name'] : ('Zona ID '.$zid); echo '<option value="'.esc_attr($zid).'">'.esc_html($zname).' (ID '.$zid.')</option>'; } echo '</select></label></p>'; // Método echo '<p><label><strong>Método en la zona:</strong> '; echo '<select name="instance_id" required>'; foreach ($zone_methods as $zid => $list) { if (empty($list)) continue; echo '<optgroup label="Zona ID '.$zid.'">'; foreach ($list as $m) { echo '<option value="'.esc_attr($m['instance_id']).'">'.esc_html($m['label']).'</option>'; } echo '</optgroup>'; } echo '</select></label></p>'; // Modo de importación echo '<p><strong>Modo de importación:</strong><br>'; echo '<label><input type="radio" name="import_mode" value="append" checked> Añadir (no borrar reglas existentes)</label><br>'; echo '<label><input type="radio" name="import_mode" value="replace"> Reemplazar (borrar reglas existentes del método seleccionado)</label>'; echo '</p>'; // Archivo echo '<p><label><strong>Archivo CSV:</strong> <input type="file" name="csv" accept=".csv" required></label></p>'; echo '<p><button class="button button-primary" type="submit" name="co_import_submit" value="1">Importar</button></p>'; echo '</form>'; echo '<h2>Ejemplo CSV</h2><pre>from,to,cost,condition,class 0,0.25,7.20,weightkg,allproducts 0.25,0.5,7.55,weightkg,allproducts 0.5,1,8.25,weightkg,allproducts </pre>'; echo '</div>'; } function co_table_has_column( $table, $col ) { global $wpdb; $exists = $wpdb->get_var( $wpdb->prepare("SHOW COLUMNS FROM `$table` LIKE %s", $col) ); return ! empty($exists); } function co_handle_import() { if ( empty($_FILES['csv']['tmp_name']) ) { echo '<div class="notice notice-error"><p>No se recibió archivo.</p></div>'; return; } $zone_id = isset($_POST['zone_id']) ? (int) $_POST['zone_id'] : 0; $instance_id = isset($_POST['instance_id']) ? (int) $_POST['instance_id'] : 0; $mode = isset($_POST['import_mode']) ? $_POST['import_mode'] : 'append'; if ( ! $instance_id ) { echo '<div class="notice notice-error"><p>Instance ID inválido.</p></div>'; return; } $file = $_FILES['csv']['tmp_name']; $handle = fopen($file, 'r'); if ( ! $handle ) { echo '<div class="notice notice-error"><p>No se pudo abrir el CSV.</p></div>'; return; } // Detectar separador $first = fgets($handle); $delimiter = (substr_count($first, ';') > substr_count($first, ',')) ? ';' : ','; rewind($handle); $rows = []; $line = 0; while ( ($data = fgetcsv($handle, 0, $delimiter)) !== false ) { $line++; // detectar cabecera if ($line === 1 && preg_match('/from/i', (string)$data[0])) continue; $data = array_pad($data, 5, ''); list($from, $to, $cost, $condition, $class) = $data; $from = floatval(str_replace(',', '.', trim($from))); $to = floatval(str_replace(',', '.', trim($to))); $cost = floatval(str_replace(',', '.', trim($cost))); $condition = $condition ? sanitize_key(trim($condition)) : 'weightkg'; // Cambia a 'weight' si tu plugin lo exige $class = $class ? sanitize_key(trim($class)) : 'allproducts'; if ($to <= $from) continue; if (! in_array($condition, ['weightkg','weight','cost'], true)) $condition = 'weightkg'; $rows[] = compact('from','to','cost','condition','class'); } fclose($handle); if ( empty($rows) ) { echo '<div class="notice notice-warning"><p>El CSV no contiene filas válidas.</p></div>'; return; } global $wpdb; $table = $wpdb->prefix . 'correos_oficial_shipping_method_rules'; // ¿Existe columna priority? $has_priority = co_table_has_column($table, 'priority'); // Contadores base según modo $base_rule = 0; $base_prio = 0; $deleted = 0; $existing = (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM `$table` WHERE instance_id=%d", $instance_id) ); if ( $mode === 'replace' ) { $wpdb->delete($table, ['instance_id' => $instance_id]); $deleted = $existing; $base_rule = 0; $base_prio = 0; } else { // append: averiguar máximos actuales para no colisionar $base_rule = (int) $wpdb->get_var( $wpdb->prepare("SELECT MAX(id_rule) FROM `$table` WHERE instance_id=%d", $instance_id) ); if ($has_priority) { $base_prio = (int) $wpdb->get_var( $wpdb->prepare("SELECT MAX(priority) FROM `$table` WHERE instance_id=%d", $instance_id) ); } else { $base_prio = $base_rule; } } // === Inserción robusta con SQL preparado (backticks en columnas reservadas) === $inserted = 0; $rule_num = $base_rule; $prio_num = $base_prio; foreach ($rows as $r) { $rule_num++; $prio_num++; $cls = $r['class'] ?: 'allproducts'; $cond = $r['condition'] ?: 'weightkg'; if ( $has_priority ) { $sql = " INSERT INTO `$table` (`instance_id`,`id_rule`,`class`,`condition`,`from`,`to`,`cost`,`priority`) VALUES (%d,%d,%s,%s,%f,%f,%f,%d) "; $prepared = $wpdb->prepare($sql, $instance_id, $rule_num, $cls, $cond, $r['from'], $r['to'], $r['cost'], $prio_num ); } else { $sql = " INSERT INTO `$table` (`instance_id`,`id_rule`,`class`,`condition`,`from`,`to`,`cost`) VALUES (%d,%d,%s,%s,%f,%f,%f) "; $prepared = $wpdb->prepare($sql, $instance_id, $rule_num, $cls, $cond, $r['from'], $r['to'], $r['cost'] ); } $ok = $wpdb->query($prepared); if ( false === $ok ) { echo '<div class="notice notice-error"><p>Error SQL: '.esc_html($wpdb->last_error).'</p></div>'; // Si quieres parar en el primer error, descomenta: // return; } else { $inserted += (int)$ok; } } $msg = 'Importación completada. Modo: ' . ( $mode === 'replace' ? 'Reemplazar' : 'Añadir' ) . '. '; if ($mode === 'replace') $msg .= 'Reglas eliminadas: ' . intval($deleted) . '. '; $msg .= 'Reglas añadidas: ' . intval($inserted) . '. '; $msg .= 'Instance ID: ' . intval($instance_id) . ' (Zona ID ' . intval($zone_id) . ').'; echo '<div class="notice notice-success"><p>'.esc_html($msg).'</p></div>'; } /* Código para hacer el diagnóstico */ add_action('admin_init', function () { if ( ! current_user_can('manage_woocommerce') || ! isset($_GET['correos_diag']) ) return; global $wpdb; echo "<pre>"; echo "=== Zonas y métodos (instance_id) ===\n"; $zones = WC_Shipping_Zones::get_zones(); $zones[] = ['zone_id' => 0, 'zone_name' => 'Resto del mundo']; foreach ($zones as $z) { $zone = new WC_Shipping_Zone($z['zone_id']); echo "\nZona {$zone->get_zone_name()} (ID {$z['zone_id']})\n"; foreach ($zone->get_shipping_methods() as $m) { echo "- instance_id={$m->instance_id} | id={$m->id} | title={$m->get_method_title()} | enabled={$m->enabled}\n"; } } echo "\n=== Tablas que parecen de Correos ===\n"; $tables = $wpdb->get_col("SHOW TABLES"); foreach ($tables as $t) { if (stripos($t,'correos')!==false) echo "$t\n"; } // Intenta detectar la tabla de reglas más probable $candidates = array_filter($tables, fn($t)=>stripos($t,'correos')!==false && stripos($t,'rule')!==false); foreach ($candidates as $tbl) { echo "\n--- Conteos por instance_id en $tbl ---\n"; $rows = $wpdb->get_results("SELECT instance_id, COUNT(*) as n FROM `$tbl` GROUP BY instance_id ORDER BY instance_id"); if ($rows) { foreach ($rows as $r) echo "instance_id={$r->instance_id} -> {$r->n} reglas\n"; } else { echo "(sin datos)\n"; } } echo "\n=== Fin ==="; echo "</pre>"; exit; });

Tutorial para importar tarifas al plugin “Correos Oficial” por peso o por coste, con o sin clases.

Qué hace y por qué

El plugin de Correos no trae importador. Este método añade una pantalla en WooCommerce Correos Importar tarifas para cargar miles de reglas desde CSV directamente al método de envío (por zona), sin tocar nada más.

Requisitos

  • WooCommerce activo.
  • Plugin Correos Oficial activo.
  • Acceso admin y WPCodeBox (o tu sistema de snippets) para pegar el código o acceso al archivo function.php
  • Tus tarifas en CSV con columnas:
    from,to,cost,condition,class

Qué permite

  • Criterio: por peso (condition=weightkg) o por coste del carrito (condition=cost).
  • Clases de envío: sin clases (productswithoutclass) o con clases (slug o “especiales”: allproducts para “pedido con varias clases”).
  • Modos: Añadir (no borra, añade) o Reemplazar (borra solo las reglas del método elegido).

Instalación del importador

  • Copia y pega el snippet PHP en WPCodeBox (o tu gestor de snippets) y actívalo.
    Archivo: importar tarifas Correos.php

Cómo preparar los CSV

A) Por peso (recomendado para paquetería)

  • Columnas: from,to,cost,condition,class
  • condition=weightkg
  • class=productswithoutclass
  • Rangos contiguos (kg): 0→1, 1→2, …
    Ejemplo:
from,to,cost,condition,class
0,1,3.21,weightkg,productswithoutclass
1,2,3.50,weightkg,productswithoutclass
2,3,3.71,weightkg,productswithoutclass

B) Por coste del carrito

  • Columnas iguales.
  • condition=cost
  • from/to en .

Ejemplo (umbral de gratis ≥50€):

from,to,cost,condition,class
0,30,9.99,cost,productswithoutclass
30,50,5.99,cost,productswithoutclass
50,999999,0.00,cost,productswithoutclass

C) Con clases de envío (opcional en otras tiendas)

  • class = slug de tu clase (p.ej. frio), o especiales:
    • productswithoutclass (productos sin clase)
    • allproducts (pedido con varias clases: fallback)

Consejo: importa primero las reglas específicas (por clase), después un fallback allproducts (quedan al final).

Uso paso a paso

  1. Crea/abre la zona y añade el método de Correos según las opciones del plugin de Correos (Paq Estándar Domicilio, Paq Ligero…).
  2. Ve a WooCommerce Correos Importar tarifas.
  3. Selecciona zona y método (Instancia: N). Fíjate en el ID de la zona, que tienes que seleccionar la instancia que le corresponda a ese ID
  4. Elige Añadir o Reemplazar.
  5. Sube el CSV
  6. Importar.

Nota: para la siguiente importación, vuelve a cargar/refrescar la página del importador.

Prompts listos para pedir a la IA los CSV desde Excel

A) Prompt por peso

Lee el Excel NOMBRE.xlsx, hoja HOJA. Genera CSV con columnas from,to,cost,condition,class para “Correos Oficial”.
Detecta tramos “Hasta X Kg” y construye intervalos: 0→1, 1→2, …
condition=weightkg, class=productswithoutclass, decimales con punto.
Un CSV por destino (Z1, Z2, Z3, Z3+, Baleares/Ceuta/Melilla, Canarias, Portugal o países AT, DE, … EU1, EU2).
Borra filas sin precio. Devuélvelos y empaquétalos en un ZIP para descargarlo.

B) Prompt por coste

Lee NOMBRE.xlsx, hoja HOJA. Genera CSV from,to,cost,condition,class por coste del carrito.
Define rangos de € (p.ej. 0–30, 30–50, ≥50 gratis).
condition=cost, class=productswithoutclass, decimales con punto.
Un CSV por zona/destino. Devuélvelos en un ZIP.

(Opcional) Si hay clases: añade CSV duplicados con class=frio, class=pesado y un fallback class=allproducts.

Prioridades y mezcla de criterios

  • Evita mezclar peso y coste en el mismo método: podrías sumar importes.
  • Si vas a cambiar de criterio, usa Reemplazar.
  • Para mantener dos lógicas, crea dos métodos distintos en la misma zona.

Diagnóstico (sin tocar la BD)

URLs útiles (estando logueado como admin y con snippets de diagnóstico activos):

  • Listar zonas/métodos/instancias: /wp-admin/?correos_diag=1
  • Ver reglas de un método: /wp-admin/?dump_rules=1&id=INSTANCIA
  • Limpiar caché de envíos: /wp-admin/?flush_ship=1

Problemas típicos

  • Instancia 0 en selector → método fantasma; añade/guarda el método real en la zona y vuelve.
  • Modal no muestra cambios → limpia caché de envíos (§8).
  • Error SQL → el importador lo muestra; revisa separador decimal (punto) y cabecera.
  • Duplicados → usa Reemplazar.
  • No aplica clase esperada → verifica class del CSV (slug correcto o productswithoutclass).