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

Generar los archivos del Libro IVA Digital de ARCA en Java

Generá en Java 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 Java

En esta guía vamos a generar desde Java 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 código 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 dentro de valores long, no como números decimales.

List<VatBreakdown> vatBreakdown = List.of(
    new VatBreakdown(
        10_000L,
        "5", // 0005 = 21%
        2_100L
    )
);

Voucher voucher = new Voucher(
    "20260701",
    "6", // 006 = Factura B
    "1",
    "123",
    "123",
    "80", // 80 = CUIT
    "30712345678",
    "CLIENTE MUÑOZ S.A.",
    12_100L,
    0L,
    0L,
    0L,
    0L,
    0L,
    0L,
    0L,
    "PES",
    "1000000", // 1,000000
    "0",
    0L,
    "00000000",
    vatBreakdown
);

Los identificadores se mantienen como String para no perder ceros iniciales. El rango admitido por los campos monetarios oficiales de 15 posiciones entra de forma segura en un long.

2. Generar los registros de ancho fijo

El ejemplo requiere Java 17 o posterior y usa únicamente la biblioteca estándar. Creá un archivo GenerarLibroIva.java con el siguiente contenido completo:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public final class GenerarLibroIva {
    private static final Charset WINDOWS_1252 =
        Charset.forName("windows-1252");

    private record VatBreakdown(
        long netCents,
        String rateCode,
        long taxCents
    ) {}

    private record Voucher(
        String date,
        String voucherType,
        String pointOfSale,
        String number,
        String numberTo,
        String buyerDocumentType,
        String buyerDocumentNumber,
        String buyerName,
        long totalCents,
        long nonTaxedCents,
        long uncategorizedPerceptionCents,
        long exemptCents,
        long nationalTaxPerceptionCents,
        long grossIncomePerceptionCents,
        long municipalPerceptionCents,
        long internalTaxCents,
        String currency,
        String exchangeRateMicros,
        String operationCode,
        long otherTaxesCents,
        String dueDate,
        List<VatBreakdown> vatBreakdown
    ) {}

    private GenerarLibroIva() {}

    private static byte[] windows1252(String value, String name) {
        CharsetEncoder encoder = WINDOWS_1252
            .newEncoder()
            .onMalformedInput(CodingErrorAction.REPORT)
            .onUnmappableCharacter(CodingErrorAction.REPORT);

        try {
            ByteBuffer encoded = encoder.encode(
                CharBuffer.wrap(value)
            );
            byte[] bytes = new byte[encoded.remaining()];
            encoded.get(bytes);
            return bytes;
        } catch (CharacterCodingException error) {
            throw new IllegalArgumentException(
                name
                    + " contiene caracteres incompatibles "
                    + "con Windows-1252",
                error
            );
        }
    }

    private static String numericField(
        String value,
        int width,
        String name
    ) {
        if (value == null || !value.matches("[0-9]+")) {
            throw new IllegalArgumentException(
                name + " debe contener solamente dígitos"
            );
        }

        if (value.length() > width) {
            throw new IllegalArgumentException(
                name + " supera las " + width + " posiciones"
            );
        }

        return "0".repeat(width - value.length()) + value;
    }

    private static String zeroPaddedAlphanumericField(
        String value,
        int width,
        String name
    ) {
        if (value == null) {
            throw new IllegalArgumentException(
                name + " debe ser alfanumérico"
            );
        }

        String text = value.trim().toUpperCase(Locale.ROOT);

        if (!text.matches("[0-9A-Z]+")) {
            throw new IllegalArgumentException(
                name + " debe ser alfanumérico"
            );
        }

        int byteLength = windows1252(text, name).length;

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

        return "0".repeat(width - byteLength) + text;
    }

    private static String textField(
        String value,
        int width,
        String name
    ) {
        if (value == null) {
            throw new IllegalArgumentException(
                name + " no puede ser nulo"
            );
        }

        String text = value
            .toUpperCase(Locale.forLanguageTag("es-AR"))
            .replaceAll("[\\r\\n\\t]", " ")
            .replaceAll("\\s+", " ")
            .trim();
        int byteLength = windows1252(text, name).length;

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

        return text + " ".repeat(width - byteLength);
    }

    private static String amountField(long cents, String name) {
        boolean negative = cents < 0;
        String value = Long.toString(cents);
        String absolute = negative ? value.substring(1) : value;
        int digitWidth = negative ? 14 : 15;

        if (absolute.length() > digitWidth) {
            throw new IllegalArgumentException(
                name + " supera las 15 posiciones"
            );
        }

        String digits =
            "0".repeat(digitWidth - absolute.length()) + absolute;
        return negative ? "-" + digits : digits;
    }

    private static String buildRecord(
        int expectedLength,
        String name,
        String... parts
    ) {
        String record = String.join("", parts);
        int byteLength = windows1252(record, name).length;

        if (byteLength != expectedLength) {
            throw new IllegalArgumentException(
                name
                    + " mide "
                    + byteLength
                    + "; ARCA exige "
                    + expectedLength
            );
        }

        return record;
    }

    private static long addExact(
        long total,
        long amount,
        String name
    ) {
        try {
            return Math.addExact(total, amount);
        } catch (ArithmeticException error) {
            throw new IllegalArgumentException(
                name + " excede el rango de long",
                error
            );
        }
    }

    private static void validateVoucher(Voucher voucher) {
        if (voucher.vatBreakdown() == null) {
            throw new IllegalArgumentException(
                "El detalle de alícuotas no puede ser nulo"
            );
        }

        int vatCount = voucher.vatBreakdown().size();

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

        long calculatedTotal = 0L;
        calculatedTotal = addExact(
            calculatedTotal,
            voucher.nonTaxedCents(),
            "El total"
        );
        calculatedTotal = addExact(
            calculatedTotal,
            voucher.uncategorizedPerceptionCents(),
            "El total"
        );
        calculatedTotal = addExact(
            calculatedTotal,
            voucher.exemptCents(),
            "El total"
        );
        calculatedTotal = addExact(
            calculatedTotal,
            voucher.nationalTaxPerceptionCents(),
            "El total"
        );
        calculatedTotal = addExact(
            calculatedTotal,
            voucher.grossIncomePerceptionCents(),
            "El total"
        );
        calculatedTotal = addExact(
            calculatedTotal,
            voucher.municipalPerceptionCents(),
            "El total"
        );
        calculatedTotal = addExact(
            calculatedTotal,
            voucher.internalTaxCents(),
            "El total"
        );
        calculatedTotal = addExact(
            calculatedTotal,
            voucher.otherTaxesCents(),
            "El total"
        );

        for (VatBreakdown vat : voucher.vatBreakdown()) {
            calculatedTotal = addExact(
                calculatedTotal,
                vat.netCents(),
                "El total"
            );
            calculatedTotal = addExact(
                calculatedTotal,
                vat.taxCents(),
                "El total"
            );
        }

        if (calculatedTotal != voucher.totalCents()) {
            throw new IllegalArgumentException(
                "El total informado ("
                    + voucher.totalCents()
                    + ") no coincide con sus componentes ("
                    + calculatedTotal
                    + ")"
            );
        }
    }

    private static String salesVoucherRecord(Voucher voucher) {
        validateVoucher(voucher);

        return buildRecord(
            266,
            "Registro de comprobante",
            numericField(voucher.date(), 8, "Fecha"),
            numericField(
                voucher.voucherType(),
                3,
                "Tipo de comprobante"
            ),
            numericField(
                voucher.pointOfSale(),
                5,
                "Punto de venta"
            ),
            numericField(
                voucher.number(),
                20,
                "Número de comprobante"
            ),
            numericField(
                voucher.numberTo(),
                20,
                "Número de comprobante hasta"
            ),
            numericField(
                voucher.buyerDocumentType(),
                2,
                "Tipo de documento"
            ),
            zeroPaddedAlphanumericField(
                voucher.buyerDocumentNumber(),
                20,
                "Documento"
            ),
            textField(
                voucher.buyerName(),
                30,
                "Nombre del comprador"
            ),
            amountField(
                voucher.totalCents(),
                "Importe total"
            ),
            amountField(
                voucher.nonTaxedCents(),
                "Conceptos no gravados"
            ),
            amountField(
                voucher.uncategorizedPerceptionCents(),
                "Percepción a no categorizados"
            ),
            amountField(
                voucher.exemptCents(),
                "Operaciones exentas"
            ),
            amountField(
                voucher.nationalTaxPerceptionCents(),
                "Percepciones nacionales"
            ),
            amountField(
                voucher.grossIncomePerceptionCents(),
                "Percepciones de Ingresos Brutos"
            ),
            amountField(
                voucher.municipalPerceptionCents(),
                "Percepciones municipales"
            ),
            amountField(
                voucher.internalTaxCents(),
                "Impuestos internos"
            ),
            textField(voucher.currency(), 3, "Moneda"),
            numericField(
                voucher.exchangeRateMicros(),
                10,
                "Tipo de cambio"
            ),
            numericField(
                Integer.toString(voucher.vatBreakdown().size()),
                1,
                "Cantidad de alícuotas"
            ),
            textField(
                voucher.operationCode(),
                1,
                "Código de operación"
            ),
            amountField(
                voucher.otherTaxesCents(),
                "Otros tributos"
            ),
            numericField(
                voucher.dueDate(),
                8,
                "Fecha de vencimiento"
            )
        );
    }

    private static String salesVatRecord(
        Voucher voucher,
        VatBreakdown vat
    ) {
        return buildRecord(
            62,
            "Registro de alícuota",
            numericField(
                voucher.voucherType(),
                3,
                "Tipo de comprobante"
            ),
            numericField(
                voucher.pointOfSale(),
                5,
                "Punto de venta"
            ),
            numericField(
                voucher.number(),
                20,
                "Número de comprobante"
            ),
            amountField(vat.netCents(), "Neto gravado"),
            numericField(vat.rateCode(), 4, "Alícuota"),
            amountField(vat.taxCents(), "Impuesto liquidado")
        );
    }

    private static void writeWindows1252File(
        Path path,
        List<String> records
    ) throws IOException {
        if (records.isEmpty()) {
            throw new IllegalArgumentException(
                path + " no contiene registros"
            );
        }

        String content = String.join("\r\n", records) + "\r\n";
        Files.write(path, windows1252(content, path.toString()));
    }

    public static void main(String[] args) throws IOException {
        List<Voucher> vouchers = List.of(
            new Voucher(
                "20260701",
                "6",
                "1",
                "123",
                "123",
                "80",
                "30712345678",
                "CLIENTE MUÑOZ S.A.",
                12_100L,
                0L,
                0L,
                0L,
                0L,
                0L,
                0L,
                0L,
                "PES",
                "1000000",
                "0",
                0L,
                "00000000",
                List.of(
                    new VatBreakdown(
                        10_000L,
                        "5",
                        2_100L
                    )
                )
            )
        );

        List<String> voucherRecords = new ArrayList<>();
        List<String> vatRecords = new ArrayList<>();

        for (Voucher voucher : vouchers) {
            voucherRecords.add(salesVoucherRecord(voucher));

            for (VatBreakdown vat : voucher.vatBreakdown()) {
                vatRecords.add(salesVatRecord(voucher, vat));
            }
        }

        writeWindows1252File(
            Path.of("LIBRO_IVA_DIGITAL_VENTAS_CBTE.txt"),
            voucherRecords
        );
        writeWindows1252File(
            Path.of("LIBRO_IVA_DIGITAL_VENTAS_ALICUOTAS.txt"),
            vatRecords
        );

        System.out.printf(
            "Generados %d comprobantes y %d alícuotas%n",
            voucherRecords.size(),
            vatRecords.size()
        );
    }
}

Compilalo y ejecutalo con:

javac -encoding UTF-8 GenerarLibroIva.java
java GenerarLibroIva

3. Validar antes de importar

El programa 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;
  • una suma excede el rango de long;
  • se intenta crear un archivo sin registros.

También escribe finales de línea CRLF y genera archivos Windows-1252 sin BOM. Estas validaciones comprueban la estructura: Portal IVA además controla el período, los códigos vigentes, la identificación del comprador, los comprobantes duplicados y las relaciones fiscales entre campos.

Casos que requieren adaptar el ejemplo

  • Varias alícuotas: agregá un objeto VatBreakdown por alícuota.
  • 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.