Editas un producto, lo guardas, y días después un cliente avisa de que otro producto distinto ha perdido su medida, su peso o su material. Nadie lo ha tocado. No es cosa de tu equipo ni de un módulo mal hecho: es un fallo del núcleo de PrestaShop que lleva ahí desde 2014 y sigue presente en la versión 9.
Cómo se manifiesta el problema
Los síntomas son siempre los mismos, y desconciertan porque parecen aleatorios:
- Una característica personalizada aparece vacía en la ficha de un producto, aunque en el back-office la característica sigue asignada.
- El producto afectado no lo ha editado nadie. El que sí se editó está perfecto.
- La tabla
ps_feature_valuetiene muchísimas más filas queps_feature_product. - El producto conserva su fila en
ps_feature_product, pero elid_feature_valueal que apunta ya no existe.
Ese último punto es la firma inconfundible: la asociación sobrevive, el valor desapareció.
La causa: valores personalizados compartidos
PrestaShop distingue dos tipos de valor de característica:
| Tipo | Campo custom | Comportamiento esperado |
|---|---|---|
| De catálogo | 0 | Se comparte entre productos. Es lo correcto: veinte productos pueden ser «Rojo» |
| Personalizado | 1 | Pertenece a un solo producto. Es el que escribes a mano en la ficha |
El problema aparece cuando varios productos acaban compartiendo un valor personalizado. En ese momento, guardar cualquiera de ellos destruye la característica de todos los demás.
La analogía: imagina que treinta productos comparten una única etiqueta de medida colgada de un hilo común. Cuando editas y guardas uno de esos treinta, PrestaShop corta y tira esa etiqueta, y le pone una nueva solo al producto que estabas editando. Los otros veintinueve se quedan con el hilo colgando y sin etiqueta.
El código exacto del núcleo
El método responsable es Product::deleteFeatures(), en classes/Product.php. Se ejecuta cada vez que guardas un producto:
foreach ($features as $tab) {
// Delete product custom features
if ($tab['custom']) {
Db::getInstance()->execute('DELETE FROM `ps_feature_value` WHERE `id_feature_value` = ' . (int) $tab['id_feature_value']);
Db::getInstance()->execute('DELETE FROM `ps_feature_value_lang` WHERE `id_feature_value` = ' . (int) $tab['id_feature_value']);
}
}
Recorre las características del producto y, para cada valor con custom = 1, lo borra de la base de datos entera. No comprueba si otros productos lo están usando. El DELETE es global.
Después vacía las asociaciones del producto y vuelve a insertarlas. Pero los demás productos conservan su fila en ps_feature_product apuntando a un identificador que acaba de desaparecer.
Por qué es tan difícil de detectar: la tabla
ps_feature_productno tieneObjectModel, así que PrestaShop no lanza ningún hook al escribir en ella. Ningún módulo puede enterarse sin recurrir a triggers de base de datos. Por eso el problema puede funcionar durante años sin que nadie lo vea.
Cómo llegan a compartirse: el webservice
Analizando el código se descartan casi todas las vías:
| Vía | ¿Puede compartir valores personalizados? |
|---|---|
| Duplicar un producto | No. Product::duplicateFeatures() los clona correctamente |
| Formulario del back-office | No. Crea un valor nuevo para cada producto |
| Importación CSV | No. FeatureValue::addFeatureValueImport() filtra por custom = 0 |
| Webservice (API) | Sí |
| Módulo que escriba por su cuenta | Sí |
El setter del webservice inserta lo que le manden, sin validar:
public function setWsProductFeatures($product_features)
{
Db::getInstance()->execute('DELETE FROM `ps_feature_product` WHERE `id_product` = ' . (int) $this->id);
foreach ($product_features as $product_feature) {
$this->addFeaturesToDB($product_feature['id'], $product_feature['id_feature_value']);
}
return true;
}
El detalle que exculpa a tu integración
Aquí está lo determinante para valorar responsabilidades. El getter del mismo webservice hace esto:
unset(
$rows[$keyrow]['id_product'],
$rows[$keyrow]['custom'] // ← se elimina el indicador "personalizado"
);
El API devuelve el identificador del valor pero suprime el campo
custom. Cualquier ERP, PIM o conector que lea las características de un producto y las escriba en otro no tiene forma de saber que está copiando un valor personalizado ajeno. Hace lo lógico —reenviar el id que el propio PrestaShop le dio— y provoca el daño sin poder evitarlo.
Es decir: si tu tienda está sincronizada con un ERP y aparece este problema, el defecto es del API de PrestaShop, no de la integración. El conector no dispone del dato que le permitiría actuar de otro modo.
Versiones afectadas
Comparando el código fuente de cuatro versiones, el comportamiento es funcionalmente idéntico en todas:
| Método | 1.6.1.24 | 1.7.8.11 | 8.2.7 | 9.1.1 |
|---|---|---|---|---|
deleteFeatures() | 1984 | 2656 | 2679 | 2417 |
getWsProductFeatures() | — | 6792 | 6920 | 6592 |
setWsProductFeatures() | 5090 | 6819 | 6947 | 6619 |
No es una regresión ni consecuencia de ninguna actualización. Existe desde PrestaShop 1.6 (2014) y sigue en la 9.1.1. La única diferencia en las versiones modernas es un filtro adicional por feature_shop para multitienda.
Cómo saber si tu tienda está afectada
Tres consultas SQL. Ejecútalas en phpMyAdmin ajustando el prefijo ps_ si el tuyo es distinto.
1. Daño ya consumado: asociaciones rotas
SELECT fp.id_product, fp.id_feature, fp.id_feature_value
FROM ps_feature_product fp
LEFT JOIN ps_feature_value fv ON fv.id_feature_value = fp.id_feature_value
WHERE fv.id_feature_value IS NULL;
Cada fila es un producto mostrando una característica vacía ahora mismo.
2. Daño latente: valores personalizados compartidos
SELECT fv.id_feature_value,
COUNT(DISTINCT fp.id_product) AS productos
FROM ps_feature_value fv
INNER JOIN ps_feature_product fp ON fp.id_feature_value = fv.id_feature_value
WHERE fv.custom = 1
GROUP BY fv.id_feature_value
HAVING productos > 1
ORDER BY productos DESC;
Son bombas sin detonar: en cuanto se guarde cualquiera de esos productos, los demás pierden la característica.
3. Indicador rápido de salud
SELECT
(SELECT COUNT(*) FROM ps_feature_value) AS valores,
(SELECT COUNT(*) FROM ps_feature_product) AS asociaciones,
ROUND((SELECT COUNT(*) FROM ps_feature_value) /
(SELECT COUNT(*) FROM ps_feature_product), 2) AS proporcion;
Una proporción por encima de 1,5 indica que se generan valores nuevos en cada guardado y los antiguos quedan huérfanos.
Solución: override gratuito para PrestaShop
Hemos publicado la corrección con licencia AFL-3.0, la misma que usan los módulos de PrestaShop. Se aplica mediante un override, así que no se pierde al actualizar.
Hemos reportado el fallo al proyecto PrestaShop en el issue #42347, con los pasos de reproducción y los parches propuestos para el núcleo. Si tu tienda está afectada, comenta ahí: cuantas más se documenten, antes se corregirá de raíz.
Qué corrige
| Método | Cambio |
|---|---|
deleteFeatures() | Un valor personalizado se borra solo si ningún otro producto lo usa |
setWsProductFeatures() | Si el API recibe un valor personalizado ajeno, lo clona en vez de compartirlo |
getWsProductFeatures() | Deja de suprimir el flag custom |
$webserviceParameters | Declara custom como campo expuesto |
Los valores de catálogo (custom = 0) no se tocan: compartirlos entre productos es su funcionamiento correcto. El primer cambio por sí solo detiene la pérdida de datos, y es retrocompatible: cuando el producto es el único dueño del valor, se borra igual que siempre.
Instalación
- Copia
override/classes/Product.phpa la carpetaoverride/classes/de tu tienda. - Borra
var/cache/prod/class_index.php(y el dedevsi existe). - Vacía la caché en Parámetros avanzados → Rendimiento.
Si ya tienes un
override/classes/Product.php, no lo sustituyas: copia los cuatro métodos dentro de la clase que ya tengas.
Reparar lo ya dañado
El override evita daños nuevos, pero no recupera lo perdido. La reparación tiene varias fases, y el orden importa:
- Copia de seguridad de la base de datos. Sin excepciones.
- Instalar el override primero. Si no, cada guardado durante la limpieza genera daño nuevo.
- Separar los valores compartidos: dar a cada producto su propia copia con el mismo texto. Esto desactiva definitivamente la causa.
- Rellenar los idiomas incompletos, si los hay.
- Limpiar las asociaciones rotas, que ya no apuntan a nada.
Lo que no se puede recuperar: en las asociaciones ya rotas, el texto original se borró también de
ps_feature_value_lang. Es irrecuperable: hay que reintroducirlo desde el ERP o desde una copia de seguridad anterior. Los valores aún compartidos sí se recuperan íntegros — por eso conviene actuar pronto: cada día que pasa, algunos se convierten en pérdidas definitivas.
Preguntas frecuentes
¿Por qué desaparecen las características de productos que no he editado?
Porque varios productos comparten el mismo valor personalizado. Al guardar uno, Product::deleteFeatures() borra ese valor de la base de datos sin comprobar si otros lo usan. Los demás quedan apuntando a un valor inexistente y su característica aparece vacía.
¿Es un fallo de una versión concreta de PrestaShop?
No. El código es funcionalmente idéntico en 1.6.1.24, 1.7.8.11, 8.2.7 y 9.1.1. No es una regresión ni consecuencia de ninguna actualización: existe desde 2014.
¿Cómo acaban varios productos compartiendo el mismo valor personalizado?
Normalmente por el webservice. setWsProductFeatures() inserta el id_feature_value que recibe sin comprobar si es personalizado ni de quién es. Y getWsProductFeatures() suprime el campo custom de la respuesta, así que la integración externa no puede saber que está reutilizando un valor ajeno.
¿Se pueden recuperar las características ya perdidas?
Solo parcialmente. Los productos que aún comparten un valor se recuperan íntegros separando los valores. Pero donde el valor ya fue borrado, el texto se eliminó también de feature_value_lang y es irrecuperable.
¿Afecta a las características normales o solo a las personalizadas?
Solo a las personalizadas (custom = 1), las que se escriben a mano en la ficha del producto. Las de catálogo, las que se eligen de una lista, no se ven afectadas: compartirlas entre productos es su funcionamiento correcto.
¿Puedo evitarlo desactivando el webservice?
Cierra la puerta a daños nuevos, pero no repara lo acumulado ni impide que los valores ya compartidos se destruyan al guardar esos productos. Hacen falta las dos cosas: el override y la reparación.
¿El problema afecta también a PrestaShop 9?
Sí. Verificado en 9.1.1: deleteFeatures() en la línea 2417 y setWsProductFeatures() en la 6619, con el mismo comportamiento que en 1.6.
¿Tu tienda tiene este problema?
Hemos desarrollado un módulo que monitoriza las seis tablas de características mediante triggers de base de datos, identifica el origen exacto de cada cambio —incluso los que no dejan rastro en PrestaShop—, detecta los productos afectados y repara el daño por lotes con simulación previa.
Escríbenos a ecomyseo@gmail.com — Ecom Experts.