📊 Import/Export CSV desde modulo — patron completo
Actualizado: 2025-01-15
#Exportar datos a CSV
Exportar productos con precios a CSV descargable
php
<?php
/**
* En getContent() del modulo — boton de exportar
*/
if (Tools::isSubmit('submitExportCSV')) {
$this->exportProductPrices();
}
/**
* Generar y descargar CSV
*/
protected function exportProductPrices(): void
{
$idLang = (int) $this->context->language->id;
$idShop = (int) $this->context->shop->id;
$products = Db::getInstance()->executeS(
(new DbQuery())
->select('p.id_product, p.reference, pl.name, p.price,
p.wholesale_price, sa.quantity,
sp.reduction, sp.reduction_type,
sp.from AS sp_from, sp.to AS sp_to')
->from('product', 'p')
->innerJoin('product_lang', 'pl',
'p.id_product = pl.id_product AND pl.id_lang = ' . $idLang)
->innerJoin('product_shop', 'ps',
'p.id_product = ps.id_product AND ps.id_shop = ' . $idShop)
->leftJoin('stock_available', 'sa',
'p.id_product = sa.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = ' . $idShop)
->leftJoin('specific_price', 'sp',
'p.id_product = sp.id_product AND sp.id_shop IN (0,' . $idShop . ')
AND sp.id_customer = 0 AND sp.id_group = 0
AND (sp.to = "0000-00-00 00:00:00" OR sp.to >= NOW())')
->where('ps.active = 1')
->orderBy('p.id_product ASC')
->build()
);
// Headers para descarga
$filename = 'productos_precios_' . date('Y-m-d_His') . '.csv';
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Pragma: no-cache');
header('Expires: 0');
$output = fopen('php://output', 'w');
// BOM UTF-8 para Excel
fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
// Cabecera
fputcsv($output, [
'ID', 'Referencia', 'Nombre', 'Precio base (sin IVA)',
'Precio coste', 'Stock', 'Descuento', 'Tipo descuento',
'Desde', 'Hasta'
], ';');
// Datos
foreach ($products as $p) {
fputcsv($output, [
$p['id_product'],
$p['reference'],
$p['name'],
number_format((float) $p['price'], 2, ',', ''),
number_format((float) $p['wholesale_price'], 2, ',', ''),
(int) $p['quantity'],
$p['reduction'] ? number_format((float) $p['reduction'], 2, ',', '') : '',
$p['reduction_type'] ?? '',
$p['sp_from'] && $p['sp_from'] !== '0000-00-00 00:00:00' ? $p['sp_from'] : '',
$p['sp_to'] && $p['sp_to'] !== '0000-00-00 00:00:00' ? $p['sp_to'] : '',
], ';');
}
fclose($output);
exit;
}
#Formulario de importacion en el BO
HelperForm con campo file para CSV
php
<?php
protected function renderImportForm(): string
{
$fields_form = [
'form' => [
'legend' => [
'title' => 'Importar precios desde CSV',
'icon' => 'icon-upload',
],
'input' => [
[
'type' => 'file',
'label' => 'Archivo CSV',
'name' => 'csv_file',
'desc' => 'Formato: ID;Referencia;Nombre;Precio;PrecioCoste;Stock (separador: punto y coma)',
],
[
'type' => 'switch',
'label' => 'Actualizar stock',
'name' => 'update_stock',
'is_bool'=> true,
'values' => [
['id' => 'stock_on', 'value' => 1, 'label' => 'Si'],
['id' => 'stock_off', 'value' => 0, 'label' => 'No'],
],
],
[
'type' => 'switch',
'label' => 'Modo simulacion (no guarda cambios)',
'name' => 'dry_run',
'is_bool'=> true,
'values' => [
['id' => 'dry_on', 'value' => 1, 'label' => 'Si'],
['id' => 'dry_off', 'value' => 0, 'label' => 'No'],
],
],
],
'submit' => [
'title' => 'Importar',
'icon' => 'process-icon-upload',
],
],
];
$helper = new HelperForm();
$helper->submit_action = 'submitImportCSV';
$helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false)
. '&configure=' . $this->name;
$helper->token = Tools::getAdminTokenLite('AdminModules');
return $helper->generateForm([$fields_form]);
}
#Procesar el CSV importado
Procesar upload y parsear CSV con validacion
php
<?php
if (Tools::isSubmit('submitImportCSV')) {
$result = $this->processImport();
if ($result['success']) {
$output .= $this->displayConfirmation(
sprintf('Importacion completada: %d actualizados, %d errores.',
$result['updated'], $result['errors'])
);
} else {
$output .= $this->displayError($result['message']);
}
}
protected function processImport(): array
{
// 1. Validar upload
if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'message' => 'Error al subir el archivo.'];
}
$file = $_FILES['csv_file'];
// 2. Validar tipo de archivo
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['csv', 'txt'])) {
return ['success' => false, 'message' => 'Solo se aceptan archivos .csv o .txt'];
}
// 3. Validar tamano (max 10 MB)
if ($file['size'] > 10 * 1024 * 1024) {
return ['success' => false, 'message' => 'El archivo supera los 10 MB.'];
}
// 4. Mover a directorio temporal seguro
$tmpDir = _PS_MODULE_DIR_ . $this->name . '/import/';
if (!is_dir($tmpDir)) {
mkdir($tmpDir, 0755, true);
}
$tmpFile = $tmpDir . uniqid('import_') . '.csv';
move_uploaded_file($file['tmp_name'], $tmpFile);
// 5. Parsear CSV
$dryRun = (bool) Tools::getValue('dry_run');
$updateStock = (bool) Tools::getValue('update_stock');
$updated = 0;
$errors = 0;
$lineNum = 0;
$handle = fopen($tmpFile, 'r');
// Detectar BOM UTF-8
$bom = fread($handle, 3);
if ($bom !== chr(0xEF) . chr(0xBB) . chr(0xBF)) {
rewind($handle);
}
// Leer cabecera
$header = fgetcsv($handle, 0, ';');
if (!$header || count($header) < 4) {
fclose($handle);
unlink($tmpFile);
return ['success' => false, 'message' => 'CSV invalido: se requieren al menos 4 columnas.'];
}
// 6. Procesar filas
while (($row = fgetcsv($handle, 0, ';')) !== false) {
$lineNum++;
if (count($row) < 4) {
$errors++;
continue;
}
$idProduct = (int) $row[0];
$price = (float) str_replace(',', '.', $row[3]);
$wholesalePrice = isset($row[4]) ? (float) str_replace(',', '.', $row[4]) : null;
$stock = isset($row[5]) ? (int) $row[5] : null;
// Validar producto existe
$product = new Product($idProduct);
if (!Validate::isLoadedObject($product)) {
$errors++;
continue;
}
if (!$dryRun) {
// Actualizar precio
$product->price = $price;
if ($wholesalePrice !== null) {
$product->wholesale_price = $wholesalePrice;
}
$product->update();
// Actualizar stock
if ($updateStock && $stock !== null) {
StockAvailable::setQuantity($idProduct, 0, $stock);
}
}
$updated++;
}
fclose($handle);
unlink($tmpFile);
return [
'success' => true,
'updated' => $updated,
'errors' => $errors,
'dry_run' => $dryRun,
];
}
#Validacion de datos
| Dato | Validacion | Funcion PS |
|---|---|---|
| ID producto | Entero positivo | (int) $value > 0 |
| Precio | Float positivo | (float) str_replace(',','.',$v) >= 0 |
| Referencia | Alfanumerico + guiones | Validate::isReference($v) |
| Nombre | Sin HTML malicioso | Validate::isCatalogName($v) |
| Stock | Entero >= 0 | (int) $value >= 0 |
| Email valido | Validate::isEmail($v) | |
| Fecha | Formato YYYY-MM-DD | Validate::isDate($v) |
#Importacion por lotes (batch)
Procesar en lotes de 100 para archivos grandes
php
<?php
/**
* Para CSVs grandes (10k+ filas), procesar en lotes evita timeout y uso excesivo de RAM
*/
protected function processInBatches(string $filePath, int $batchSize = 100): array
{
$handle = fopen($filePath, 'r');
$header = fgetcsv($handle, 0, ';');
$batch = [];
$totalUpdated = 0;
$totalErrors = 0;
while (($row = fgetcsv($handle, 0, ';')) !== false) {
$batch[] = $row;
if (count($batch) >= $batchSize) {
$result = $this->processBatch($batch);
$totalUpdated += $result['updated'];
$totalErrors += $result['errors'];
$batch = [];
// Liberar memoria
if (function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
}
}
// Procesar ultimo lote
if (!empty($batch)) {
$result = $this->processBatch($batch);
$totalUpdated += $result['updated'];
$totalErrors += $result['errors'];
}
fclose($handle);
return ['updated' => $totalUpdated, 'errors' => $totalErrors];
}
protected function processBatch(array $rows): array
{
$updated = 0;
$errors = 0;
// Usar transaccion SQL para el lote
Db::getInstance()->execute('START TRANSACTION');
try {
foreach ($rows as $row) {
$idProduct = (int) $row[0];
$price = (float) str_replace(',', '.', $row[3]);
// UPDATE directo (mas rapido que ObjectModel para lotes)
$success = Db::getInstance()->update('product', [
'price' => $price,
], 'id_product = ' . $idProduct);
// Tambien actualizar product_shop
Db::getInstance()->update('product_shop', [
'price' => $price,
], 'id_product = ' . $idProduct);
$success ? $updated++ : $errors++;
}
Db::getInstance()->execute('COMMIT');
} catch (Exception $e) {
Db::getInstance()->execute('ROLLBACK');
$errors += count($rows);
$updated = 0;
}
return ['updated' => $updated, 'errors' => $errors];
}
#Ejemplo completo: import/export de precios
Patron profesional
Este patron incluye: modo simulacion (dry_run), BOM UTF-8 para Excel, separador configurable, procesamiento por lotes, transacciones SQL, y limpieza de archivos temporales. Es el mismo patron usado en modulos de importacion profesionales.
Descargar en Markdown
Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.