---
title: Import/Export CSV desde modulo — patron completo
section: examples
slug: csv-import-export
description: "Importar y exportar datos CSV desde modulos PrestaShop: upload seguro, parsing, validacion, procesamiento por lotes, descarga con headers correctos."
keywords: prestashop csv import export modulo upload archivo parsing lotes batch
last_updated: 2025-01-15
source_url: "https://ayudaprestashop.es/examples/csv-import-export"
---

# Import/Export CSV desde modulo — patron completo

> Importar y exportar datos CSV desde modulos PrestaShop: upload seguro, parsing, validacion, procesamiento por lotes, descarga con headers correctos.

## 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 | 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

> **[TIP] 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.


---

*Fuente: [https://ayudaprestashop.es/examples/csv-import-export](https://ayudaprestashop.es/examples/csv-import-export). Version Markdown generada automaticamente para consumo por LLMs.*
