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

Generar los archivos del Libro IVA Digital de ARCA en .NET con C#

Generá en C# 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 .NET con C#

En esta guía vamos a generar con C# y .NET 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. Crear el proyecto

El ejemplo usa solamente clases incluidas en .NET, sin paquetes externos:

dotnet new console -n LibroIvaDigital
cd LibroIvaDigital

2. Preparar los datos

Vamos a generar una Factura B por $121: $100 de neto gravado y $21 de IVA.

El modelo conserva los importes como centavos en valores long. Al recibir montos decimales, ToCents los convierte de forma exacta: no usa double, no redondea silenciosamente y rechaza más de dos decimales. La cotización sigue el mismo criterio, pero con seis decimales.

3. Generar los registros de ancho fijo

Reemplazá el contenido de Program.cs por este código completo:

using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

internal static class Program
{
    private static readonly CultureInfo ArgentineCulture =
        CultureInfo.GetCultureInfo("es-AR");

    private static readonly Encoding Windows1252 = CreateWindows1252();

    private static void Main()
    {
        var vouchers = new List<Voucher>
        {
            new()
            {
                Date = "20260701",
                VoucherType = 6, // 006 = Factura B
                PointOfSale = "1",
                Number = "123",
                NumberTo = "123",
                BuyerDocumentType = 80, // 80 = CUIT
                BuyerDocumentNumber = "30712345678",
                BuyerName = "CLIENTE MUÑOZ S.A.",
                TotalCents = ToCents(121.00m, "Importe total"),
                NonTaxedCents = 0,
                UncategorizedPerceptionCents = 0,
                ExemptCents = 0,
                NationalTaxPerceptionCents = 0,
                GrossIncomePerceptionCents = 0,
                MunicipalPerceptionCents = 0,
                InternalTaxCents = 0,
                Currency = "PES",
                ExchangeRateMicros = ToExchangeRateMicros(
                    1.000000m,
                    "Tipo de cambio"
                ),
                OperationCode = "0",
                OtherTaxesCents = 0,
                DueDate = "00000000",
                VatBreakdown = new[]
                {
                    new VatLine(
                        NetCents: ToCents(100.00m, "Neto gravado"),
                        RateCode: 5, // 0005 = 21%
                        TaxCents: ToCents(21.00m, "IVA liquidado")
                    )
                }
            }
        };

        var voucherRecords = new List<string>();
        var vatRecords = new List<string>();

        foreach (var voucher in vouchers)
        {
            voucherRecords.Add(SalesVoucherRecord(voucher));

            foreach (var vat in voucher.VatBreakdown)
            {
                vatRecords.Add(SalesVatRecord(voucher, vat));
            }
        }

        WriteWindows1252File(
            "LIBRO_IVA_DIGITAL_VENTAS_CBTE.txt",
            voucherRecords,
            266
        );
        WriteWindows1252File(
            "LIBRO_IVA_DIGITAL_VENTAS_ALICUOTAS.txt",
            vatRecords,
            62
        );

        Console.WriteLine(
            $"Generados {voucherRecords.Count} comprobantes y " +
            $"{vatRecords.Count} alícuotas"
        );
    }

    private static Encoding CreateWindows1252()
    {
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

        return Encoding.GetEncoding(
            1252,
            EncoderFallback.ExceptionFallback,
            DecoderFallback.ExceptionFallback
        );
    }

    private static byte[] EncodeWindows1252(string value, string name)
    {
        try
        {
            return Windows1252.GetBytes(value);
        }
        catch (EncoderFallbackException exception)
        {
            throw new ArgumentException(
                $"{name} contiene caracteres incompatibles con Windows-1252",
                name,
                exception
            );
        }
    }

    private static string NumericField(
        object value,
        int width,
        string name
    )
    {
        var text = Convert.ToString(
            value,
            CultureInfo.InvariantCulture
        ) ?? string.Empty;

        if (text.Length == 0 || text.Any(character =>
                character is < '0' or > '9'))
        {
            throw new ArgumentException(
                $"{name} debe contener solamente dígitos",
                name
            );
        }

        if (text.Length > width)
        {
            throw new ArgumentException(
                $"{name} supera las {width} posiciones",
                name
            );
        }

        return text.PadLeft(width, '0');
    }

    private static string ZeroPaddedAlphanumericField(
        string value,
        int width,
        string name
    )
    {
        var text = value.Trim().ToUpperInvariant();

        if (text.Length == 0 || text.Any(character =>
                !(
                    character is >= '0' and <= '9' ||
                    character is >= 'A' and <= 'Z'
                )))
        {
            throw new ArgumentException(
                $"{name} debe ser alfanumérico",
                name
            );
        }

        var byteLength = EncodeWindows1252(text, name).Length;

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

        return new string('0', width - byteLength) + text;
    }

    private static string TextField(
        string value,
        int width,
        string name
    )
    {
        var text = Regex
            .Replace(
                value.Replace('\r', ' ')
                    .Replace('\n', ' ')
                    .Replace('\t', ' '),
                @"\s+",
                " "
            )
            .Trim()
            .ToUpper(ArgentineCulture);

        var byteLength = EncodeWindows1252(text, name).Length;

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

        return text + new string(' ', width - byteLength);
    }

    private static string AmountField(long cents, string name)
    {
        var text = cents.ToString(CultureInfo.InvariantCulture);
        var isNegative = text.StartsWith(
            "-",
            StringComparison.Ordinal
        );
        var absolute = isNegative ? text[1..] : text;
        var digitWidth = isNegative ? 14 : 15;

        if (absolute.Length > digitWidth)
        {
            throw new ArgumentException(
                $"{name} supera las 15 posiciones",
                name
            );
        }

        var digits = absolute.PadLeft(digitWidth, '0');
        return isNegative ? $"-{digits}" : digits;
    }

    private static long ToCents(decimal amount, string name)
    {
        var scaled = amount * 100m;

        if (scaled != decimal.Truncate(scaled))
        {
            throw new ArgumentException(
                $"{name} tiene más de dos decimales",
                name
            );
        }

        return checked((long)scaled);
    }

    private static long ToExchangeRateMicros(
        decimal exchangeRate,
        string name
    )
    {
        var scaled = exchangeRate * 1_000_000m;

        if (scaled != decimal.Truncate(scaled))
        {
            throw new ArgumentException(
                $"{name} tiene más de seis decimales",
                name
            );
        }

        return checked((long)scaled);
    }

    private static string BuildRecord(
        IEnumerable<string> parts,
        int expectedLength,
        string name
    )
    {
        var record = string.Concat(parts);
        var byteLength = EncodeWindows1252(record, name).Length;

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

        return record;
    }

    private static void ValidateVoucher(Voucher voucher)
    {
        var vatCount = voucher.VatBreakdown.Count;

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

        long calculatedTotal = 0;

        checked
        {
            calculatedTotal += voucher.NonTaxedCents;
            calculatedTotal += voucher.UncategorizedPerceptionCents;
            calculatedTotal += voucher.ExemptCents;
            calculatedTotal += voucher.NationalTaxPerceptionCents;
            calculatedTotal += voucher.GrossIncomePerceptionCents;
            calculatedTotal += voucher.MunicipalPerceptionCents;
            calculatedTotal += voucher.InternalTaxCents;
            calculatedTotal += voucher.OtherTaxesCents;

            foreach (var vat in voucher.VatBreakdown)
            {
                calculatedTotal += vat.NetCents;
                calculatedTotal += vat.TaxCents;
            }
        }

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

    private static string SalesVoucherRecord(Voucher voucher)
    {
        ValidateVoucher(voucher);

        return BuildRecord(
            new[]
            {
                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(
                    voucher.VatBreakdown.Count,
                    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"
                )
            },
            266,
            "Registro de comprobante"
        );
    }

    private static string SalesVatRecord(
        Voucher voucher,
        VatLine vat
    )
    {
        return BuildRecord(
            new[]
            {
                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")
            },
            62,
            "Registro de alícuota"
        );
    }

    private static void WriteWindows1252File(
        string path,
        IReadOnlyCollection<string> records,
        int recordWidth
    )
    {
        if (records.Count == 0)
        {
            throw new ArgumentException(
                $"{path} no contiene registros",
                nameof(records)
            );
        }

        var content = string.Join("\r\n", records) + "\r\n";
        var bytes = EncodeWindows1252(content, path);
        var expectedLength = checked(
            records.Count * (recordWidth + 2)
        );

        if (bytes.Length != expectedLength)
        {
            throw new ArgumentException(
                $"{path} mide {bytes.Length}; " +
                $"se esperaban {expectedLength} bytes"
            );
        }

        File.WriteAllBytes(path, bytes);
    }
}

internal sealed class Voucher
{
    public string Date { get; init; } = string.Empty;
    public int VoucherType { get; init; }
    public string PointOfSale { get; init; } = string.Empty;
    public string Number { get; init; } = string.Empty;
    public string NumberTo { get; init; } = string.Empty;
    public int BuyerDocumentType { get; init; }
    public string BuyerDocumentNumber { get; init; } = string.Empty;
    public string BuyerName { get; init; } = string.Empty;
    public long TotalCents { get; init; }
    public long NonTaxedCents { get; init; }
    public long UncategorizedPerceptionCents { get; init; }
    public long ExemptCents { get; init; }
    public long NationalTaxPerceptionCents { get; init; }
    public long GrossIncomePerceptionCents { get; init; }
    public long MunicipalPerceptionCents { get; init; }
    public long InternalTaxCents { get; init; }
    public string Currency { get; init; } = string.Empty;
    public long ExchangeRateMicros { get; init; }
    public string OperationCode { get; init; } = string.Empty;
    public long OtherTaxesCents { get; init; }
    public string DueDate { get; init; } = string.Empty;
    public IReadOnlyList<VatLine> VatBreakdown { get; init; } =
        Array.Empty<VatLine>();
}

internal sealed record VatLine(
    long NetCents,
    int RateCode,
    long TaxCents
);

Ejecutalo con:

dotnet run

4. 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;
  • un importe tiene más de dos decimales o la cotización más de seis;
  • se intenta crear un archivo sin registros.

Los archivos se escriben sin BOM, con finales de línea CRLF. Cada registro se controla después de codificarlo a Windows-1252, por lo que el ancho validado es el de bytes que recibe ARCA y no la cantidad de caracteres de .NET.

Estas comprobaciones son estructurales. Portal IVA también valida el período seleccionado, las tablas de códigos vigentes, la identificación del comprador, los comprobantes duplicados y las correlaciones fiscales.

Casos que requieren adaptar el ejemplo

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