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

Generar los archivos del Libro IVA Digital de ARCA en Python

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

En esta guía vamos a generar desde Python 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.

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 a la lista. El generador mantiene el mismo orden en ambos archivos.

2. Generar los registros de ancho fijo

El ejemplo usa únicamente módulos de la biblioteca estándar. Creá un archivo generar_libro_iva.py con el siguiente contenido completo:

from pathlib import Path
import re


def numeric_field(value: int | str, width: int, name: str) -> str:
    text = str(value)

    if re.fullmatch(r"[0-9]+", text) is None:
        raise ValueError(f"{name} debe contener solamente dígitos")

    if len(text) > width:
        raise ValueError(f"{name} supera las {width} posiciones")

    return text.zfill(width)


def zero_padded_alphanumeric_field(
    value: int | str,
    width: int,
    name: str,
) -> str:
    text = str(value).strip().upper()

    if re.fullmatch(r"[0-9A-Z]+", text) is None:
        raise ValueError(f"{name} debe ser alfanumérico")

    try:
        byte_length = len(text.encode("windows-1252"))
    except UnicodeEncodeError as error:
        raise ValueError(
            f"{name} contiene caracteres incompatibles "
            "con Windows-1252"
        ) from error

    if byte_length > width:
        raise ValueError(f"{name} supera las {width} posiciones")

    return ("0" * (width - byte_length)) + text


def text_field(value: str, width: int, name: str) -> str:
    text = re.sub(r"[\r\n\t]", " ", str(value).upper())
    text = re.sub(r"\s+", " ", text).strip()

    try:
        byte_length = len(text.encode("windows-1252"))
    except UnicodeEncodeError as error:
        raise ValueError(
            f"{name} contiene caracteres incompatibles "
            "con Windows-1252"
        ) from error

    if byte_length > width:
        raise ValueError(f"{name} supera las {width} posiciones")

    return text + (" " * (width - byte_length))


def amount_field(cents: int, name: str) -> str:
    if not isinstance(cents, int):
        raise ValueError(f"{name} debe expresarse en centavos enteros")

    absolute = str(abs(cents))
    digit_width = 14 if cents < 0 else 15

    if len(absolute) > digit_width:
        raise ValueError(f"{name} supera las 15 posiciones")

    digits = absolute.zfill(digit_width)
    return f"-{digits}" if cents < 0 else digits


def build_record(
    parts: list[str],
    expected_length: int,
    name: str,
) -> str:
    record = "".join(parts)

    try:
        byte_length = len(record.encode("windows-1252"))
    except UnicodeEncodeError as error:
        raise ValueError(
            f"{name} contiene caracteres incompatibles "
            "con Windows-1252"
        ) from error

    if byte_length != expected_length:
        raise ValueError(
            f"{name} mide {byte_length}; "
            f"ARCA exige {expected_length}"
        )

    return record


def validate_voucher(voucher: dict) -> None:
    vat_count = len(voucher["vat_breakdown"])

    if not 1 <= vat_count <= 9:
        raise ValueError(
            "La cantidad de alícuotas debe estar entre 1 y 9"
        )

    vat_total = sum(
        vat["net_cents"] + vat["tax_cents"]
        for vat in voucher["vat_breakdown"]
    )

    calculated_total = (
        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"]
        + vat_total
    )

    if calculated_total != voucher["total_cents"]:
        raise ValueError(
            f"El total informado ({voucher['total_cents']}) "
            f"no coincide con sus componentes ({calculated_total})"
        )


def sales_voucher_record(voucher: dict) -> str:
    validate_voucher(voucher)

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


def sales_vat_record(voucher: dict, vat: dict) -> str:
    return build_record(
        [
            numeric_field(
                voucher["voucher_type"],
                3,
                "Tipo de comprobante",
            ),
            numeric_field(
                voucher["point_of_sale"],
                5,
                "Punto de venta",
            ),
            numeric_field(
                voucher["number"],
                20,
                "Número de comprobante",
            ),
            amount_field(vat["net_cents"], "Neto gravado"),
            numeric_field(vat["rate_code"], 4, "Alícuota"),
            amount_field(vat["tax_cents"], "Impuesto liquidado"),
        ],
        62,
        "Registro de alícuota",
    )


def write_windows_1252_file(
    path: str,
    records: list[str],
) -> None:
    if not records:
        raise ValueError(f"{path} no contiene registros")

    content = "\r\n".join(records) + "\r\n"
    Path(path).write_bytes(content.encode("windows-1252"))


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,
            }
        ],
    }
]

voucher_records = [
    sales_voucher_record(voucher)
    for voucher in vouchers
]
vat_records = [
    sales_vat_record(voucher, vat)
    for voucher in vouchers
    for vat in voucher["vat_breakdown"]
]

write_windows_1252_file(
    "LIBRO_IVA_DIGITAL_VENTAS_CBTE.txt",
    voucher_records,
)
write_windows_1252_file(
    "LIBRO_IVA_DIGITAL_VENTAS_ALICUOTAS.txt",
    vat_records,
)

print(
    f"Generados {len(voucher_records)} comprobantes "
    f"y {len(vat_records)} alícuotas"
)

Ejecutalo con:

python generar_libro_iva.py

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;
  • un importe no fue entregado en centavos enteros;
  • 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 diccionario 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.