Merchgain
ES
Disponible ahora
Próximamente, en este orden
  • FrançaisFR
  • ItalianoIT
  • PortuguêsPT
  • NederlandsNL

Un idioma aparece aquí cuando sus páginas están escritas de verdad. Ningún hablante nativo ha revisado todavía los textos en español. ¿Has encontrado un error? Escríbenos.

Instalar gratisInstalar

Tus costes ya están ahí, pero en el campo equivocado

El trabajo ya lo hiciste. Los valores están en un campo del que el núcleo de WooCommerce no sabe nada, así que el informe muestra un hueco donde están tus datos. Esta página te ayuda a pasarlos al campo correcto, después de enseñarte exactamente lo que haría.

Actualizado 2026-10-23

Si el aviso de costes que faltan te dijo que algunos de tus productos guardan un coste en otro plugin, esta es la página a la que apuntaba. Nada de lo que hay aquí es específico de Merchgain: el script escribe en el campo que ofrece el núcleo de WooCommerce, así que funciona igual tanto si sigues con nuestro plugin como si no.

Esta página da por hecho que la función de precio de coste ya está activada. Si no lo está, actívala primero: mientras el interruptor esté desactivado, los valores escritos en el campo del núcleo no llegan a ningún pedido.

Pasar los costes arregla el futuro, no el pasado. WooCommerce escribe el coste en la línea del pedido en el momento de la venta, así que los pedidos hechos antes del traspaso se quedan sin coste para siempre. No hay relleno hacia atrás y no es un descuido. Es un motivo para hacerlo esta semana y no el próximo trimestre.

Qué campos lee el script

Dónde se guarda el costePlugin, con su propio nombre
_alg_wc_cog_costCost of Goods: Product Cost & Profit Calculator for WooCommerce (WPFactory)
_wc_cog_costWooCommerce Cost of Goods (SkyVerge, se vende en woocommerce.com)
_wcj_purchase_priceBooster for WooCommerce: su módulo Cost of Goods
_izettle_cost_priceBjornTech PayPal POS integration for WooCommerce
_ni_cost_goodsNi Cost of Goods for WooCommerce
cost_of_goodsCost Of Goods For WooCommerce
tabla atum_product_data, columna purchase_priceATUM Inventory Management for WooCommerce

Estas ubicaciones se encontraron en el propio código de esos plugins, en la línea en la que cada uno guarda el valor, no en una constante, en una etiqueta de formulario ni en un historial de cambios. Esa distinción no es pedantería, y ya ha salvado esta página más de una vez. El plugin de WPFactory anunció una versión con el «prefijo actualizado a wpfcogs», que cambió el prefijo de sus funciones y opciones y no la clave del metadato. ATUM llama a su campo de formulario _purchase_price, pero desde la versión 1.5.0 (diciembre de 2018) guarda el valor en una tabla propia. Un script escrito a partir de cualquiera de las dos cosas buscaría un campo que no existe y devolvería un cero sin más. La única excepción es WooCommerce Cost of Goods: es una extensión de pago sin código público, así que su clave sale de su propia documentación, confirmada por los plugins que importan desde ella.

Corrección, 24 de septiembre de 2026. Hasta ese día, este script leía el metadato _purchase_price de ATUM. ATUM no escribe ahí desde la 1.5.0: en una tienda que usaba ATUM antes, esas filas son copias congeladas el día en que ATUM llevó sus datos a su propia tabla, y en una tienda más nueva ni siquiera existen. Si ejecutaste el script anterior en una tienda con ATUM y llegó a pasar algún coste, compara esos costes con los precios de compra de ATUM antes de fiarte del informe.

Dos de ellos merecen un comentario propio. Booster puede añadir «gastos extra» y campos de coste personalizados encima del precio de compra; el script pasa solo el precio de compra, porque el envío y extras parecidos no son coste de los productos (si tú sí los cuentas, añádelos a mano). WooCommerce Cost of Goods, desde la versión 2.15.0, copia sus costes al campo del núcleo por sí mismo, según su historial de cambios; en una tienda que lo use, el script informará sobre todo de costes que ya están ahí.

Si tus costes están en otro sitio, la forma del script sigue valiendo: añade tu clave a la lista de arriba y comprueba qué significa el valor antes de ejecutarlo (neto o bruto, por unidad o por línea). Un campo de coste que resulta haber guardado precios brutos es peor que no tener campo de coste.

El script

Lee, decide y no escribe nada hasta que cambies la primera línea. Guárdalo como migrate-costs.php y ejecútalo con WP-CLI.

<?php
/**
 * Moves costs kept by another plugin into WooCommerce's native cost field.
 * Run with:  wp eval-file migrate-costs.php
 */

$DRY_RUN = true;   // <- switch to false only after a successful trial

// Fields kept in post meta: meta key => plugin.
$KEYS = array(
    '_alg_wc_cog_cost'    => 'Cost of Goods: Product Cost & Profit Calculator for WooCommerce',
    '_wc_cog_cost'        => 'WooCommerce Cost of Goods',
    '_wcj_purchase_price' => 'Booster for WooCommerce',
    '_izettle_cost_price' => 'BjornTech PayPal POS integration for WooCommerce',
    '_ni_cost_goods'      => 'Ni Cost of Goods for WooCommerce',
    'cost_of_goods'       => 'Cost Of Goods For WooCommerce',
);

// Plugins with a table of their own: table without prefix => id column, cost column.
$TABLES = array(
    'atum_product_data' => array( 'product_id', 'purchase_price' ),   // ATUM Inventory Management for WooCommerce
);

global $wpdb;

$found = array();

$placeholders = implode( ', ', array_fill( 0, count( $KEYS ), '%s' ) );
$rows         = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT post_id, meta_key, meta_value
           FROM {$wpdb->postmeta}
          WHERE meta_key IN ( {$placeholders} )
            AND meta_value <> ''
            AND CAST( meta_value AS DECIMAL( 20, 6 ) ) > 0",
        array_keys( $KEYS )
    ),
    ARRAY_A
);
foreach ( (array) $rows as $row ) {
    $found[ (int) $row['post_id'] ][ $row['meta_key'] ] = $row['meta_value'];
}

foreach ( $TABLES as $table => $columns ) {
    list( $id_column, $cost_column ) = $columns;
    $name = $wpdb->prefix . $table;
    if ( $name !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $name ) ) ) ) {
        continue;                       // that plugin has never been installed here
    }
    $rows = $wpdb->get_results(
        "SELECT {$id_column} AS id, {$cost_column} AS cost
           FROM {$name}
          WHERE {$cost_column} IS NOT NULL
            AND CAST( {$cost_column} AS DECIMAL( 20, 6 ) ) > 0",
        ARRAY_A
    );
    foreach ( (array) $rows as $row ) {
        $found[ (int) $row['id'] ][ "{$table}.{$cost_column}" ] = $row['cost'];
    }
}

$moved = 0;
$owned = 0;
$parts = 0;
$held  = 0;
$clash = 0;
$gone  = 0;

foreach ( $found as $id => $values ) {
    $product = wc_get_product( $id );
    if ( ! $product ) {
        WP_CLI::warning( "No product with id {$id} - leftover data from something deleted." );
        $gone++;
        continue;
    }

    // A variation's cost can replace the parent's or be added on top of it.
    // That is a decision about what the number means, so the script leaves it alone.
    if ( $product->is_type( 'variation' ) ) {
        WP_CLI::log( sprintf( '[variation] %d %s: %s - left for you',
            $id, $product->get_name(), implode( ', ', $values ) ) );
        $parts++;
        continue;
    }

    $current = $product->get_cogs_value();
    if ( null !== $current && '' !== $current ) {
        $owned++;                       // never overwrite a cost already in the core field
        continue;
    }

    // Compared as numbers: "12.50" in one field and "12.5" in another are the same cost.
    $numbers = array();
    foreach ( $values as $value ) {
        $numbers[] = (string) round( (float) $value, 6 );
    }
    if ( count( array_unique( $numbers ) ) > 1 ) {
        $shown = array();
        foreach ( $values as $key => $value ) {
            $shown[] = "{$key}={$value}";
        }
        WP_CLI::warning( sprintf( 'Two costs disagree on %d %s: %s - skipped',
            $id, $product->get_name(), implode( ', ', $shown ) ) );
        $clash++;
        continue;
    }

    $cost = (float) reset( $values );

    // A variable product's cost is inherited by every variation without a cost of its
    // own in WooCommerce. If one of them has a DIFFERENT cost in the old plugin - or an
    // explicit zero in the same field - moving the parent would quietly give it the
    // parent's number instead. Such a parent is held back until its variations are set.
    if ( $product->is_type( 'variable' ) ) {
        $differ = array();
        foreach ( $product->get_children() as $child_id ) {
            $child = wc_get_product( $child_id );
            if ( ! $child || null !== $child->get_cogs_value() ) {
                continue;               // a cost of its own in WooCommerce is not inherited
            }
            $theirs = isset( $found[ $child_id ] ) ? $found[ $child_id ] : array();
            foreach ( array_keys( $values ) as $key ) {
                $zero = isset( $KEYS[ $key ] ) ? get_post_meta( $child_id, $key, true ) : '';
                if ( '' !== $zero && 0.0 === (float) $zero ) {
                    $theirs[ $key ] = '0';
                }
            }
            foreach ( $theirs as $value ) {
                if ( (string) round( (float) $value, 6 ) !== (string) round( $cost, 6 ) ) {
                    $differ[] = $child_id;
                    break;
                }
            }
        }
        if ( array() !== $differ ) {
            WP_CLI::warning( sprintf( 'Variable product %d %s: variations %s have a different cost in the old plugin, and moving %s would give them this one - held back, set those variations first',
                $id, $product->get_name(), implode( ', ', $differ ), $cost ) );
            $held++;
            continue;
        }
    }

    if ( $DRY_RUN ) {
        WP_CLI::log( sprintf( '[dry run] %d %s: -> %s (from %s)',
            $id, $product->get_name(), $cost, key( $values ) ) );
    } else {
        $product->set_cogs_value( $cost );
        $product->save();
        WP_CLI::log( sprintf( '%d %s = %s', $id, $product->get_name(), $cost ) );
    }
    $moved++;
}

WP_CLI::success( sprintf(
    '%s: %d | already had a cost: %d | variations left for you: %d | held back: %d | disagreements: %d | missing products: %d',
    $DRY_RUN ? 'To move' : 'Moved', $moved, $owned, $parts, $held, $clash, $gone
) );
wp eval-file migrate-costs.php

El script está aquí para leerlo, no para descargarlo con un clic, y es a propósito. Escribe en tu base de datos, así que debería pasar por tus ojos antes de pasar por tus productos.

Cómo leer el modo de prueba

La última línea es todo el informe, y cada cifra significa algo sobre lo que quizá tengas que actuar. Los mensajes del script están en inglés; entre paréntesis, el texto que verás.

  • Para pasar (To move): productos que recibirán un coste. Es la única cifra que cambia algo.
  • Ya tenían coste (already had a cost): el campo del núcleo está relleno, así que el script no lo toca. Si esta cifra cubre todo tu catálogo y no queda nada por pasar, has terminado y el informe medía otra cosa; comprueba que la función esté activada.
  • Variaciones que te tocan a ti (variations left for you): mira más abajo. El script nunca las toca.
  • Retenidos (held back): un producto variable cuyo propio coste el script no pasó porque algunas de sus variaciones tienen un coste distinto en el plugin antiguo. En WooCommerce, una variación sin coste propio usa el de su padre, así que pasar el del padre les daría en silencio esa cifra. El aviso las nombra; configúralas primero y vuelve a ejecutar el script.
  • Desacuerdos (disagreements): el mismo producto tiene costes distintos en los campos de dos plugins. Nadie puede saber cuál es el actual: ninguno tiene fecha. El script los imprime todos con los nombres de los campos para que los resuelvas en el editor de productos, donde ves lo que eliges. Los costes se comparan como números, así que 12.50 en un campo y 12.5 en otro no son un desacuerdo.
  • Productos que faltan (missing products): metadatos que dejaron productos borrados. No hacen daño, y no merece la pena limpiarlos en la misma pasada que algo que escribe.

Un cero en un campo ajeno no se pasa, ni tampoco un importe negativo. En WooCommerce, un cero guardado y un coste que falta son lo mismo, así que pasar un cero no cambia nada salvo el número de filas de tu base de datos; un coste negativo es un error de datos que hay que arreglar en el origen, no un valor que copiar.

Las variaciones se dejan a propósito

Una variación hereda el coste del producto padre hasta que tiene uno propio, y cuando lo tiene, un interruptor decide si ese coste sustituye al del padre o se suma a él. Una camiseta que cuesta 24 con una variación bordada a 3 sale a 27 en modo de suma y a 3 en modo de sustitución. Es una decisión sobre lo que significa la cifra, no sobre cuál es, y un script no puede tomarla a partir de los datos: el campo ajeno lleva un valor y ningún modo.

Así que deja que el script rellene los productos padre y después abre las variaciones que ha listado y configúralas a propósito. El coste propio de un producto variable solo se pasa cuando ninguna de sus variaciones dice otra cosa en el plugin antiguo; si no, se retiene, como arriba. Cómo funcionan los costes en las variaciones es la versión corta.

ATUM no tiene campo de precio de compra en el propio producto variable, solo en cada una de sus variaciones, así que una tienda con ATUM y productos variables verá esas filas en esta lista y no en la cifra de pasados. No es un fallo del script, sino prudencia.

La prueba que no conviene saltarse

  1. Una copia de seguridad de la base de datos. No una exportación de productos: una copia de seguridad. Guardar un producto toca más que el único campo que te interesa.
  2. El modo de prueba, leído línea a línea. Sobre todo los desacuerdos: son la única parte de esto que necesita a una persona.
  3. Después, $DRY_RUN = false.
  4. Comprueba en la tienda, no en la terminal. Haz un pedido de prueba de un producto que acaba de recibir un coste y comprueba que el pedido lo guardó. Es la única comprobación que cubre toda la cadena y no solo la ficha del producto.

Después del traspaso

Conserva los datos del plugin antiguo. No se gana nada borrándolos, y siguen siendo tu único registro de lo que costaban las cosas. Si más adelante desactivas el plugin antiguo, sus valores se quedan en la base de datos; el script se puede volver a ejecutar y simplemente informará de que todo tiene ya coste.

Vuelve a ejecutarlo después de una importación. Cualquier cosa que escriba en el campo del plugin antiguo (una hoja del proveedor, una edición masiva, una sincronización de inventario) vuelve a poner valores donde el informe no los ve. Volver a ejecutar el script es seguro: nunca sobrescribe un coste que ya esté en el campo del núcleo.

Si además rellenas costes desde una hoja de cálculo, la importación masiva de costes es la otra mitad del mismo trabajo.

Lo que esto no arregla

  • El historial. Los pedidos hechos antes del traspaso se quedan sin coste para siempre. Por qué no hay relleno hacia atrás.
  • Los reembolsos. Al hacer un reembolso, WooCommerce recalcula el coste de cada línea del pedido original con el coste actual del producto, así que los valores recién pasados pueden reescribir el mismo historial que describen. Qué pasa entonces.
  • Los ceros de verdad. Un producto que de verdad no cuesta nada se guarda como si no tuviera coste, así que se queda en la lista de costes que faltan y nada de aquí lo cambia.
  • Los costes variables. El envío, las comisiones de pago y las devoluciones al proveedor no son coste de los productos. El beneficio bruto no es el beneficio neto, y no fingimos lo contrario.

Si tu tienda se comporta de forma distinta a lo que describe esta página, merece la pena que nos escribas: estas páginas se corrigen cuando resultan estar equivocadas.