Resolución N° 5.616/2024. Más información

Generar los archivos del Libro IVA Digital de ARCA en PHP

Generá en PHP los TXT de ventas y alícuotas que acepta Portal IVA, con ancho fijo y codificación Windows-1252.


Generar los archivos del Libro IVA Digital de ARCA en PHP

En esta guía vamos a generar desde PHP los dos archivos de ventas que acepta la importación de Portal IVA:

  • LIBRO_IVA_DIGITAL_VENTAS_CBTE.txt, con una línea de 266 bytes por comprobante;
  • LIBRO_IVA_DIGITAL_VENTAS_ALICUOTAS.txt, con una línea de 62 bytes por cada alícuota.

Este ejemplo es para responsables inscriptos que importan ventas en IVA Simple. Los sujetos exentos tienen un diseño específico, sin este archivo de alícuotas, y no deben usar el script sin adaptarlo.

Si primero querés entender qué es el Libro IVA, cómo se relaciona con IVA Simple y por qué el ZIP de duplicados electrónicos es diferente, leé la guía general:

Cómo generar los archivos del Libro IVA Digital de ARCA

Cómo generar los archivos del Libro IVA Digital de ARCA

Qué es el Libro IVA Digital, qué archivos acepta Portal IVA y cómo generar e importar el Libro IVA Ventas desde tu sistema.

1. Preparar los datos

El ejemplo usa una Factura B por $121: $100 de neto gravado y $21 de IVA. Los importes se guardan en centavos enteros, no como números decimales.

El script requiere PHP de 64 bits para cubrir todo el rango de los campos de 15 posiciones. En una instalación de 32 bits, representá los importes como strings y operalos con BCMath.

$vouchers = [
    [
        'date' => '20260701',
        'voucher_type' => 6, // 006 = Factura B
        'point_of_sale' => '1',
        'number' => '123',
        'number_to' => '123',
        'buyer_document_type' => 80, // 80 = CUIT
        'buyer_document_number' => '30712345678',
        'buyer_name' => 'CLIENTE MUÑOZ S.A.',
        'total_cents' => 12100,
        'non_taxed_cents' => 0,
        'uncategorized_perception_cents' => 0,
        'exempt_cents' => 0,
        'national_tax_perception_cents' => 0,
        'gross_income_perception_cents' => 0,
        'municipal_perception_cents' => 0,
        'internal_tax_cents' => 0,
        'currency' => 'PES',
        'exchange_rate_micros' => 1000000, // 1,000000
        'operation_code' => '0',
        'other_taxes_cents' => 0,
        'due_date' => '00000000',
        'vat_breakdown' => [
            [
                'net_cents' => 10000,
                'rate_code' => 5, // 0005 = 21%
                'tax_cents' => 2100,
            ],
        ],
    ],
];

Podés agregar más comprobantes al arreglo. El generador mantiene el mismo orden en ambos archivos.

2. Generar los registros de ancho fijo

La extensión iconv debe estar habilitada para convertir el resultado a Windows-1252. Creá un archivo generar-libro-iva.php con el siguiente contenido completo:

<?php

declare(strict_types=1);

if (PHP_INT_SIZE < 8) {
    throw new RuntimeException(
        'Este ejemplo requiere PHP de 64 bits'
    );
}

function windows1252(string $value, string $name): string
{
    $encoded = @iconv('UTF-8', 'Windows-1252', $value);

    if ($encoded === false) {
        throw new InvalidArgumentException(
            "$name contiene caracteres incompatibles con Windows-1252"
        );
    }

    return $encoded;
}

function numericField(int|string $value, int $width, string $name): string
{
    $text = (string) $value;

    if (preg_match('/^\d+$/D', $text) !== 1) {
        throw new InvalidArgumentException(
            "$name debe contener solamente dígitos"
        );
    }

    if (strlen($text) > $width) {
        throw new InvalidArgumentException(
            "$name supera las $width posiciones"
        );
    }

    return str_pad($text, $width, '0', STR_PAD_LEFT);
}

function zeroPaddedAlphanumericField(
    int|string $value,
    int $width,
    string $name
): string {
    $text = strtoupper(trim((string) $value));

    if (preg_match('/^[0-9A-Z]+$/D', $text) !== 1) {
        throw new InvalidArgumentException(
            "$name debe ser alfanumérico"
        );
    }

    $byteLength = strlen(windows1252($text, $name));

    if ($byteLength > $width) {
        throw new InvalidArgumentException(
            "$name supera las $width posiciones"
        );
    }

    return str_pad($text, $width, '0', STR_PAD_LEFT);
}

function textField(string $value, int $width, string $name): string
{
    $text = function_exists('mb_strtoupper')
        ? mb_strtoupper($value, 'UTF-8')
        : strtoupper($value);
    $text = preg_replace('/[\r\n\t]/', ' ', $text);
    $text = preg_replace('/\s+/u', ' ', $text ?? '');
    $text = trim($text ?? '');
    $byteLength = strlen(windows1252($text, $name));

    if ($byteLength > $width) {
        throw new InvalidArgumentException(
            "$name supera las $width posiciones"
        );
    }

    return $text . str_repeat(' ', $width - $byteLength);
}

function amountField(int $cents, string $name): string
{
    $absolute = (string) abs($cents);
    $digitWidth = $cents < 0 ? 14 : 15;

    if (strlen($absolute) > $digitWidth) {
        throw new InvalidArgumentException(
            "$name supera las 15 posiciones"
        );
    }

    $digits = str_pad($absolute, $digitWidth, '0', STR_PAD_LEFT);
    return $cents < 0 ? "-$digits" : $digits;
}

function buildRecord(
    array $parts,
    int $expectedLength,
    string $name
): string {
    $record = implode('', $parts);
    $byteLength = strlen(windows1252($record, $name));

    if ($byteLength !== $expectedLength) {
        throw new RuntimeException(
            "$name mide $byteLength" .
            "; ARCA exige $expectedLength"
        );
    }

    return $record;
}

function validateVoucher(array $voucher): void
{
    $vatCount = count($voucher['vat_breakdown']);

    if ($vatCount < 1 || $vatCount > 9) {
        throw new InvalidArgumentException(
            'La cantidad de alícuotas debe estar entre 1 y 9'
        );
    }

    $vatTotal = array_sum(array_map(
        fn (array $vat): int =>
            $vat['net_cents'] + $vat['tax_cents'],
        $voucher['vat_breakdown']
    ));

    $calculatedTotal =
        $voucher['non_taxed_cents'] +
        $voucher['uncategorized_perception_cents'] +
        $voucher['exempt_cents'] +
        $voucher['national_tax_perception_cents'] +
        $voucher['gross_income_perception_cents'] +
        $voucher['municipal_perception_cents'] +
        $voucher['internal_tax_cents'] +
        $voucher['other_taxes_cents'] +
        $vatTotal;

    if ($calculatedTotal !== $voucher['total_cents']) {
        throw new InvalidArgumentException(
            "El total informado ({$voucher['total_cents']}) " .
            "no coincide con sus componentes ($calculatedTotal)"
        );
    }
}

function salesVoucherRecord(array $voucher): string
{
    validateVoucher($voucher);

    return buildRecord(
        [
            numericField($voucher['date'], 8, 'Fecha'),
            numericField(
                $voucher['voucher_type'],
                3,
                'Tipo de comprobante'
            ),
            numericField(
                $voucher['point_of_sale'],
                5,
                'Punto de venta'
            ),
            numericField(
                $voucher['number'],
                20,
                'Número de comprobante'
            ),
            numericField(
                $voucher['number_to'],
                20,
                'Número de comprobante hasta'
            ),
            numericField(
                $voucher['buyer_document_type'],
                2,
                'Tipo de documento'
            ),
            zeroPaddedAlphanumericField(
                $voucher['buyer_document_number'],
                20,
                'Documento'
            ),
            textField(
                $voucher['buyer_name'],
                30,
                'Nombre del comprador'
            ),
            amountField($voucher['total_cents'], 'Importe total'),
            amountField(
                $voucher['non_taxed_cents'],
                'Conceptos no gravados'
            ),
            amountField(
                $voucher['uncategorized_perception_cents'],
                'Percepción a no categorizados'
            ),
            amountField(
                $voucher['exempt_cents'],
                'Operaciones exentas'
            ),
            amountField(
                $voucher['national_tax_perception_cents'],
                'Percepciones nacionales'
            ),
            amountField(
                $voucher['gross_income_perception_cents'],
                'Percepciones de Ingresos Brutos'
            ),
            amountField(
                $voucher['municipal_perception_cents'],
                'Percepciones municipales'
            ),
            amountField(
                $voucher['internal_tax_cents'],
                'Impuestos internos'
            ),
            textField($voucher['currency'], 3, 'Moneda'),
            numericField(
                $voucher['exchange_rate_micros'],
                10,
                'Tipo de cambio'
            ),
            numericField(
                count($voucher['vat_breakdown']),
                1,
                'Cantidad de alícuotas'
            ),
            textField(
                $voucher['operation_code'],
                1,
                'Código de operación'
            ),
            amountField(
                $voucher['other_taxes_cents'],
                'Otros tributos'
            ),
            numericField(
                $voucher['due_date'],
                8,
                'Fecha de vencimiento'
            ),
        ],
        266,
        'Registro de comprobante'
    );
}

function salesVatRecord(array $voucher, array $vat): string
{
    return buildRecord(
        [
            numericField(
                $voucher['voucher_type'],
                3,
                'Tipo de comprobante'
            ),
            numericField(
                $voucher['point_of_sale'],
                5,
                'Punto de venta'
            ),
            numericField(
                $voucher['number'],
                20,
                'Número de comprobante'
            ),
            amountField($vat['net_cents'], 'Neto gravado'),
            numericField($vat['rate_code'], 4, 'Alícuota'),
            amountField($vat['tax_cents'], 'Impuesto liquidado'),
        ],
        62,
        'Registro de alícuota'
    );
}

function writeWindows1252File(string $path, array $records): void
{
    if ($records === []) {
        throw new InvalidArgumentException(
            "$path no contiene registros"
        );
    }

    $content = implode("\r\n", $records) . "\r\n";
    $encoded = windows1252($content, $path);

    if (file_put_contents($path, $encoded) === false) {
        throw new RuntimeException("No se pudo escribir $path");
    }
}

$vouchers = [
    [
        'date' => '20260701',
        'voucher_type' => 6,
        'point_of_sale' => '1',
        'number' => '123',
        'number_to' => '123',
        'buyer_document_type' => 80,
        'buyer_document_number' => '30712345678',
        'buyer_name' => 'CLIENTE MUÑOZ S.A.',
        'total_cents' => 12100,
        'non_taxed_cents' => 0,
        'uncategorized_perception_cents' => 0,
        'exempt_cents' => 0,
        'national_tax_perception_cents' => 0,
        'gross_income_perception_cents' => 0,
        'municipal_perception_cents' => 0,
        'internal_tax_cents' => 0,
        'currency' => 'PES',
        'exchange_rate_micros' => 1000000,
        'operation_code' => '0',
        'other_taxes_cents' => 0,
        'due_date' => '00000000',
        'vat_breakdown' => [
            [
                'net_cents' => 10000,
                'rate_code' => 5,
                'tax_cents' => 2100,
            ],
        ],
    ],
];

$voucherRecords = array_map('salesVoucherRecord', $vouchers);
$vatRecords = [];

foreach ($vouchers as $voucher) {
    foreach ($voucher['vat_breakdown'] as $vat) {
        $vatRecords[] = salesVatRecord($voucher, $vat);
    }
}

writeWindows1252File(
    'LIBRO_IVA_DIGITAL_VENTAS_CBTE.txt',
    $voucherRecords
);
writeWindows1252File(
    'LIBRO_IVA_DIGITAL_VENTAS_ALICUOTAS.txt',
    $vatRecords
);

echo 'Generados ' . count($voucherRecords) .
    ' comprobantes y ' . count($vatRecords) .
    " alícuotas\n";

Ejecutalo con:

php generar-libro-iva.php

3. Validar antes de importar

El script corta la ejecución si:

  • un campo numérico contiene caracteres no permitidos;
  • el documento del comprador no es alfanumérico;
  • un valor supera el ancho oficial;
  • un texto contiene caracteres incompatibles con Windows-1252;
  • una cabecera no mide exactamente 266 bytes;
  • una alícuota no mide exactamente 62 bytes;
  • el total no coincide con el neto, IVA y demás conceptos;
  • se intenta crear un archivo sin registros.

También escribe finales de línea CRLF y convierte el contenido a Windows-1252 sin BOM.

Casos que requieren adaptar el ejemplo

  • Varias alícuotas: agregá un arreglo por alícuota en vat_breakdown.
  • Operaciones exentas o no gravadas: usá el código de operación y la alícuota que indique la tabla oficial.
  • Notas de crédito: informá su tipo de comprobante y no inviertas los importes automáticamente.
  • Moneda extranjera: mantené consistente la moneda, el tipo de cambio y la opción elegida al importar.
  • TURIVA, importaciones y comprobantes anulados: usá sus diseños específicos.

Otras versiones

Consultá siempre el diseño de registros, las validaciones y las tablas del sistema vigentes antes de generar archivos productivos.

Conéctate a ARCA hoy mismo

Evitá la complejidad de ARCA. Con Afip SDK integrás tu sistema en minutos y te enfocás en hacer crecer tu negocio.