BEAR - WooCommerce Bulk Editor and Products Manager Professional

Convertir atributos personalizados de producto en globales en WooCommerce

Los productos importados desde un archivo CSV o trasladados desde Shopify suelen recibir su Talla, Color o Material como atributos personalizados de producto: texto simple escrito en cada producto en lugar de valores de un atributo compartido. Se muestran en la página del producto, pero ni WooCommerce ni BEAR pueden filtrarlos, renombrarlos ni editarlos en varios productos a la vez. La solución es convertir el atributo personalizado de producto en un atributo global. A continuación se explica cómo reconocer los dos tipos, qué puede y qué no puede hacer BEAR con los personalizados, cómo convertirlos en masa sin perder las variaciones y cómo eliminar los restos.

Atributos globales y personalizados (locales) en WooCommerce

WooCommerce tiene dos tipos de atributos de producto, y se guardan de formas distintas.

  • Los atributos globales se crean una sola vez en Productos → Atributos. Cada uno es una taxonomía llamada pa_ más su slug (pa_size, pa_color), y sus valores son términos de esa taxonomía, compartidos por todos los productos que los usan. Si renombras el término XL una vez, todos los productos muestran el nombre nuevo.
  • Los atributos personalizados, también llamados locales o de texto, existen solo dentro de un producto. El nombre y los valores son texto guardado en el meta _product_attributes de ese producto, por ejemplo S | M | L. Dos productos con un atributo personalizado llamado Talla no comparten más que la ortografía.

En el editor de productos, en la pestaña Atributos, un atributo global muestra su nombre como una etiqueta fija y sus valores como un selector de términos. Un atributo personalizado muestra el nombre como un campo de texto editable y los valores en un cuadro de texto separados por |. La guía de WooCommerce sobre taxonomías y atributos de producto muestra ambos.

Las importaciones son el origen habitual de los atributos personalizados. El importador de productos de WooCommerce crea un atributo global solo cuando la columna Attribute 1 global (2, 3...) vale 1; con 0 o sin la columna, cada producto recibe su propio atributo personalizado, aunque ya exista un atributo global con el mismo nombre. Por eso una tienda puede acabar con dos atributos «Talla» en un mismo producto. Las herramientas de migración desde Shopify y otras plataformas suelen hacer lo mismo. Para evitar que vuelva a ocurrir, establece la columna global en 1 para todos los atributos de tus archivos de importación.

Qué puede y qué no puede hacer BEAR con los atributos personalizados

BEAR edita los atributos como taxonomías. Cada atributo global es una columna que puedes activar en la pestaña Ajustes (Settings), un filtro en la pestaña Filtros (Filters), una fila en la pestaña Edición en masa (Bulk Edit) y una opción en Operaciones avanzadas en masa sobre variaciones (Variations Advanced Bulk Operations). Los atributos personalizados no son nada de eso, porque no existe una lista de ellos: cada producto guarda su propio texto privado. En la práctica:

  • No hay columna ni filtro para un atributo personalizado, así que no puedes listar «todos los productos con Material = Algodón».
  • No hay cambio masivo de sus nombres, valores ni visibilidad, ni eliminación masiva.
  • Las variaciones basadas en un atributo personalizado se pueden seguir editando en la tabla (precios, stock, SKU), pero no se pueden seleccionar por valor de atributo. Filtrar o fijar precios de variaciones por talla o color, como se describe en filtrar variaciones por atributo, requiere un atributo global.
  • Generar variaciones a partir de combinaciones de atributos, como se describe en crear variaciones en masa, usa solo atributos globales. Añadir una Talla global a productos cuyas variaciones usan una Talla personalizada les da un segundo cuadro de Talla independiente, en lugar de un valor nuevo en el existente.

Los mismos límites se aplican en otras partes de WooCommerce: los filtros por atributo de la página de la tienda y las páginas de archivo de atributos también necesitan atributos globales. Solo por estos motivos ya merece la pena convertir.

Convertir en masa atributos personalizados de producto en globales

Productos sin variaciones: dos ejecuciones

Cuando el atributo personalizado es solo informativo (Material, Marca, Estampado) y su valor va unido a algo que BEAR puede filtrar, como una categoría, BEAR puede hacer la mayor parte del trabajo. El propio valor personalizado no se puede filtrar, así que esta vía necesita ese vínculo.

  1. Crea el atributo global y sus términos en Productos → Atributos.
  2. Selecciona los productos en BEAR, por ejemplo por categoría, y añade el atributo global con el término correcto, como se describe en añadir atributos a productos en masa. Los productos con valores distintos requieren una ejecución por valor, cada una con su propio filtro.
  3. Elimina el antiguo atributo personalizado con el segundo ejemplo de código de más abajo.

Si los valores difieren de un producto a otro, o hay variaciones basadas en el atributo, usa en su lugar el código de conversión: lee los valores propios de cada producto.

Variaciones o valores distintos: convertir en el sitio

Una variación no apunta al atributo; guarda su valor bajo una clave. Para una Talla personalizada la clave es attribute_size y el valor es el texto, como Small. Para una Talla global la clave es attribute_pa_size y el valor es el slug del término, small. Convertir significa, por tanto, reescribir el producto padre y cada variación. Eliminar las variaciones y generarlas de nuevo también funciona, pero las variaciones nuevas reciben IDs nuevos, y hay que volver a establecer sus precios, stock, SKU e imágenes.

El código siguiente convierte un atributo personalizado en el sitio. Para cada producto que lo tiene, el código crea los términos que falten a partir de los valores del producto, sustituye el atributo personalizado por el global en la misma posición y con los mismos indicadores Visible en la página de productos (Visible on the product page) y Usado para variaciones (Used for variations), traslada los valores predeterminados del formulario y reescribe el valor de cada variación. Los IDs de las variaciones, los precios, el stock y los SKU se mantienen.

/*
 * Convert one custom (local) product attribute into a global attribute,
 * variations included. Child theme functions.php or a code snippets plugin.
 * Create the global attribute in Products > Attributes first, back up the
 * database, then open /wp-admin/?convert_local_attribute=1 as an administrator.
 * Remove the code when it reports that it has finished.
 */
add_action('admin_init', function () {
    if (empty($_GET['convert_local_attribute']) || !current_user_can('manage_woocommerce')) {
        return;
    }

    $local_name = 'Size';    // the custom attribute name as typed on the products
    $taxonomy   = 'pa_size'; // the global attribute: pa_ + its slug
    $per_page   = 50;

    if (!taxonomy_exists($taxonomy)) {
        wp_die('Create the global attribute first: ' . esc_html($taxonomy));
    }

    $local_key = sanitize_title($local_name);
    $page      = max(1, (int) $_GET['convert_local_attribute']);
    $converted = 0;
    $skipped   = 0;

    // Finds or creates the term for a value, returns the term object.
    $get_term = function ($value) use ($taxonomy) {
        $found = term_exists($value, $taxonomy);
        if (!$found) {
            $found = wp_insert_term($value, $taxonomy);
        }
        return is_wp_error($found) ? null : get_term((int) $found['term_id'], $taxonomy);
    };

    $result = wc_get_products(array(
        'status'   => array('publish', 'private', 'draft', 'pending', 'future'),
        'limit'    => $per_page,
        'page'     => $page,
        'orderby'  => 'ID',
        'order'    => 'ASC',
        'paginate' => true,
    ));

    foreach ($result->products as $product) {
        $attributes = $product->get_attributes();

        if (!isset($attributes[$local_key]) || $attributes[$local_key]->is_taxonomy()) {
            continue;
        }
        if (isset($attributes[$taxonomy])) {
            $skipped++; // has both: fix this product by hand
            continue;
        }

        $local    = $attributes[$local_key];
        $term_ids = array();
        foreach ($local->get_options() as $value) {
            $term = $get_term($value);
            if ($term) {
                $term_ids[] = (int) $term->term_id;
            }
        }

        $global = new WC_Product_Attribute();
        $global->set_id(wc_attribute_taxonomy_id_by_name($taxonomy));
        $global->set_name($taxonomy);
        $global->set_options($term_ids);
        $global->set_position($local->get_position());
        $global->set_visible($local->get_visible());
        $global->set_variation($local->get_variation());

        unset($attributes[$local_key]);
        $attributes[$taxonomy] = $global;
        $product->set_attributes($attributes);

        // The default form values of a variable product use the same keys.
        $defaults = $product->get_default_attributes();
        if (isset($defaults[$local_key])) {
            $term = $get_term($defaults[$local_key]);
            unset($defaults[$local_key]);
            if ($term) {
                $defaults[$taxonomy] = $term->slug;
            }
            $product->set_default_attributes($defaults);
        }
        $product->save();

        // Every variation stores its own value: text before, term slug now.
        foreach ($product->is_type('variable') ? $product->get_children() : array() as $variation_id) {
            $variation = wc_get_product($variation_id);
            $values    = $variation ? $variation->get_attributes() : array();
            if (!array_key_exists($local_key, $values)) {
                continue;
            }
            $value = $values[$local_key];
            unset($values[$local_key]);
            $term = ('' === $value) ? null : $get_term($value);
            $values[$taxonomy] = $term ? $term->slug : ''; // empty = "Any"
            $variation->set_attributes($values);
            $variation->save();
        }

        $converted++;
    }

    $next = $page < $result->max_num_pages
        ? '<a href="' . esc_url(admin_url('?convert_local_attribute=' . ($page + 1))) . '">Next page</a>'
        : 'Finished. Remove this code.';

    wp_die(sprintf(
        'Page %d of %d: %d products converted, %d skipped (they already have %s). %s',
        $page,
        max(1, $result->max_num_pages),
        $converted,
        $skipped,
        esc_html($taxonomy),
        $next
    ));
});

Cómo ejecutarlo:

  1. Haz una copia de seguridad completa de la base de datos. El código cambia los productos directamente; el Historial de BEAR no lo registra y no puede deshacerlo.
  2. Crea el atributo global en Productos → Atributos, con el mismo nombre, y anota su slug. Los términos los crea el código.
  3. Establece $local_name con el nombre del atributo personalizado exactamente como figura en los productos, y $taxonomy con pa_ más el slug.
  4. Con la sesión iniciada como administrador, abre /wp-admin/?convert_local_attribute=1. Cada página procesa 50 productos y muestra un enlace a la siguiente; síguelo hasta que el mensaje diga que ha terminado.
  5. Revisa unos cuantos productos, simples y variables, en el frontend y en el editor de productos. Después elimina el código.

Los productos que ya tienen tanto el atributo personalizado como el global se omiten y se cuentan, porque fusionar ambos exige decidir qué valores son los correctos. Corrígelos a mano o elimina el duplicado personalizado con el ejemplo siguiente. Con WPML o Polylang, los productos traducidos tienen sus propios nombres de atributo; ejecuta la conversión para cada nombre y traduce los términos nuevos. Para varios atributos (Talla y Color), ejecútalo una vez por atributo.

Eliminar en masa los atributos personalizados sobrantes

Después de reconstruir las variaciones de un producto sobre atributos globales, o después de que una importación haya creado duplicados, los antiguos atributos personalizados permanecen en los productos. Este código elimina de todos los productos los atributos personalizados indicados por nombre. Los atributos globales con el mismo nombre no se tocan, y se conserva un atributo personalizado sobre el que aún se basan variaciones existentes, de modo que ninguna variación pierde su opción.

/*
 * Remove custom (local) attributes by name from all products.
 * Attributes that existing variations are built on are kept.
 * Child theme functions.php or a code snippets plugin. Back up the database,
 * then open /wp-admin/?delete_local_attributes=1 as an administrator.
 */
add_action('admin_init', function () {
    if (empty($_GET['delete_local_attributes']) || !current_user_can('manage_woocommerce')) {
        return;
    }

    $names   = array_map('sanitize_title', array('Size', 'Color')); // custom attribute names
    $page    = max(1, (int) $_GET['delete_local_attributes']);
    $cleaned = 0;

    $result = wc_get_products(array(
        'status'   => array('publish', 'private', 'draft', 'pending', 'future'),
        'limit'    => 50,
        'page'     => $page,
        'orderby'  => 'ID',
        'order'    => 'ASC',
        'paginate' => true,
    ));

    foreach ($result->products as $product) {
        $attributes = $product->get_attributes();
        $changed    = false;

        foreach ($attributes as $key => $attribute) {
            if ($attribute->is_taxonomy() || !in_array(sanitize_title($attribute->get_name()), $names, true)) {
                continue;
            }
            if ($attribute->get_variation() && $product->is_type('variable') && $product->get_children()) {
                continue; // variations still use it
            }
            unset($attributes[$key]);
            $changed = true;
        }

        if ($changed) {
            $product->set_attributes($attributes);
            $product->save();
            $cleaned++;
        }
    }

    $next = $page < $result->max_num_pages
        ? '<a href="' . esc_url(admin_url('?delete_local_attributes=' . ($page + 1))) . '">Next page</a>'
        : 'Finished. Remove this code.';

    wp_die(sprintf('Page %d of %d: %d products cleaned. %s', $page, max(1, $result->max_num_pages), $cleaned, $next));
});

Haz antes una copia de seguridad, enumera los nombres en $names, abre después /wp-admin/?delete_local_attributes=1 como administrador y sigue los enlaces como antes. Un producto que sigue teniendo el atributo personalizado tras la ejecución tiene variaciones basadas en él; conviértelo o elimina antes esas variaciones. Para quitar un atributo global de los productos, consulta cómo desvincular por completo un atributo.

Después de la conversión

  • Abre un producto variable convertido en el editor: el atributo aparece como un selector de términos, y cada variación muestra su valor en lugar de Alguno (Any).
  • En BEAR, activa la columna del atributo en la pestaña Ajustes y filtra por uno de sus términos: aparecen los productos convertidos.
  • Renombra o fusiona valores una sola vez, en Productos → Atributos → Configurar términos (Configure terms), en lugar de hacerlo en cada producto.
  • Los títulos de las variaciones se reconstruyen al guardar el código cada variación, ahora a partir de los nombres de los términos; títulos de variaciones con atributos explica cómo se construyen.
  • Vacía la caché de páginas si la tienda sigue mostrando las tablas de atributos antiguas.

Si un producto no se convierte como esperas, publica la configuración de sus atributos y el mensaje que imprimió el código en el foro de soporte de BEAR.