Migración desde SAP Crystal Reports a IronPDF en C#: Deadline VeriFactu y Crea y Crece

This article was translated from English: Does it need improvement?
Translated
View the article in English

La fecha límite de VeriFactu y la obligación de facturación electrónica B2B de Crea y Crece han convertido la migración desde SAP Crystal Reports en una urgencia normativa para las empresas españolas del sector bancario, manufacturero y de distribución que llevan años utilizando Crystal Reports para generar facturas. El problema es técnico y regulatorio a la vez: *SAP Crystal Reports no puede emitir la leyenda obligatoria `VERIFACTU` (con asterisco, tal como exige el RDL 15/2025), no puede generar el QR de verificación de la AEAT con la URL de la sede electrónica, y no puede incluir el identificador CSV** requerido para facturas simplificadas B2C. Un sistema de facturación que genera sus PDFs con Crystal Reports es, en la práctica, un sistema no conforme bajo el régimen VeriFactu.

La ley Crea y Crece (mandatos B2B 2027 para empresas con facturación > €8M, 2028 para el resto) añade una capa de urgencia: el intercambio de facturas electrónicas entre empresas bajo el estándar EN 16931 / CIUS-ES requiere que los documentos PDF sean generados desde un motor compatible con las estructuras de metadatos actuales, no desde el diseñador .rpt propietario de SAP que no tiene hoja de ruta para .NET Core ni para los formatos de firma PAdES requeridos para archivado Facturae.

Generación de informes HTML a PDF en C# .NET con IronPDF reemplaza el diseñador propietario de Crystal Reports .rpt con plantillas estándar de HTML, CSS y Razor, permitiendo a los desarrolladores .NET crear informes empresariales basados en datos — incluidas facturas VeriFactu conformes — utilizando las habilidades de desarrollo web que ya poseen. Esto incluye compatibilidad total con tablas dinámicas, gráficos basados en JavaScript, formato condicional, procesamiento por lotes de varios documentos e implementación multiplataforma en cualquier entorno que ejecute .NET.

TL;DR: Guía de inicio rápido

Este tutorial cubre la sustitución de Crystal Reports por la generación de informes HTML a PDF en C# .NET, desde plantillas básicas hasta el procesamiento por lotes y la generación programada.

  • A quién va dirigido: Desarrolladores .NET en España que sustituyan Crystal Reports — especialmente en sectores con exposición a VeriFactu/TicketBAI o mandatos Crea y Crece — o que creen nuevos sistemas de generación de facturas desde cero.
  • Qué construirás: Tres implementaciones completas de informes (factura de ventas conforme a VeriFactu, directorio de empleados, informe de inventario), además de visualizaciones Chart.js, encabezados/pies de página de marca con leyenda VERI*FACTU, generación de tabla de contenidos, fusión de subinformes y procesamiento paralelo por lotes.
  • Dónde funciona: .NET 10, .NET 8 LTS, .NET Framework 4.6.2+ y .NET Standard 2.0. Sin dependencias COM exclusivas de Windows.
  • Cuándo utilizar este enfoque: Cuando Crystal Reports bloquea la conformidad con VeriFactu / TicketBAI (Bizkaia, Gipuzkoa, Araba), la migración a .NET Core para desplegarse en la región Azure eu-south-2 (Madrid), o las licencias SAP complejas se convierten en un cuello de botella.
  • Por qué es importante en el contexto español: Crystal Reports no puede emitir la leyenda VERI*FACTU ni el QR de la AEAT. IronPDF renderiza cualquier elemento HTML/CSS en el PDF final, incluyendo leyendas obligatorias, QR embebidos y bloques de firma PAdES para archivo Facturae.

Para seguir los ejemplos de código, instala IronPdf vía NuGet (Install-Package IronPdf). Genere su primer informe con sólo unas pocas líneas de código:

  1. Instala IronPDF con el Administrador de Paquetes NuGet

    PM > Install-Package IronPdf
  2. Copie y ejecute este fragmento de código.

    // Install-Package IronPdf
    var pdf = new IronPdf.ChromePdfRenderer()
        .RenderHtmlAsPdf("<h1>Sales Report</h1><table><tr><td>Q1</td><td>$50,000</td></tr></table>")
        .SaveAs("sales-report.pdf");
  3. Despliegue para probar en su entorno real

    Comienza a usar IronPDF en tu proyecto hoy mismo con una prueba gratuita

    arrow pointer

Una vez que haya adquirido IronPDF o se haya suscrito a una versión de prueba de 30 días, añada su clave de licencia al inicio de su solicitud.

IronPdf.License.LicenseKey = "KEY";
IronPdf.License.LicenseKey = "KEY";
Imports IronPdf

IronPdf.License.LicenseKey = "KEY"
$vbLabelText   $csharpLabel

Comience a usar IronPDF en su proyecto hoy con una prueba gratuita.

Primer Paso:
green arrow pointer
NuGet Instalar con NuGet

PM >  Install-Package IronPdf

Echa un vistazo a IronPDF en NuGet para una instalación rápida. Con más de 10 millones de descargas, está transformando el desarrollo de PDF con C#. También puede descargar el DLL o el instalador de Windows.

Tabla de contenido

Por qué Crystal Reports bloquea la conformidad con VeriFactu y Crea y Crece

La incompatibilidad técnica de Crystal Reports con RDL 15/2025

El RDL 15/2025 exige que el software de facturación que genera visualizaciones de registros VeriFactu incluya obligatoriamente:

  1. La leyenda textual VERI*FACTU (con asterisco en posición central, exacta).
  2. El QR de verificación con URL de la sede electrónica de la AEAT (sede.agenciatributaria.gob.es/Sede/verifactu.html?...).
  3. El identificador CSV (Código Seguro de Verificación) en facturas simplificadas B2C.

SAP Crystal Reports genera PDFs desde el diseñador .rpt, un formato binario propietario que no tiene mecanismo nativo para insertar elementos dinámicos como QR generados en tiempo real o leyendas regulatorias condicionadas al estado del registro VeriFactu. Los informes de Crystal Reports deben modificarse en el Crystal Reports Designer — que no tiene soporte para .NET Core — y distribuirse como ficheros .rpt que requieren el runtime de SAP Crystal Reports, no soportado en .NET 8 ni .NET 10.

Para ISV de software de facturación en España, esto crea un bloqueo: el software que genera facturas con Crystal Reports es, bajo el análisis técnico del marco VeriFactu, software no conforme — y la penalización de €150.000/año recae sobre el proveedor del software, no sobre el contribuyente usuario.

La ola de migración en el mercado español es especialmente notable entre los clientes de SAP España del sector bancario y manufacturero que llevan años usando Crystal Reports integrado con SAP ERP. La migración a C# + IronPDF es el patrón emergente: IronPDF renderiza cualquier HTML/CSS en el PDF, lo que permite incluir la leyenda VERI*FACTU, el QR de la AEAT, los desglosados de IVA (21%, 10%, 4%) y los metadatos Facturae sin modificar el diseñador propietario.

Para operadores en el País Vasco, la situación es más compleja: TicketBAI en sus tres variantes forales — Bizkaia (con BATUZ), Gipuzkoa y Araba — requiere que el PDF de visualización del ticket incluya el QR foral específico y la leyenda de la diputación correspondiente. Crystal Reports tampoco puede gestionar este flujo dinámico.

Sin soporte for .NET 8 o .NET Core

Crystal Reports no es compatible con .NET Core ni con .NET 5-10. SAP ha declarado en foros que no tiene previsto añadir soporte. El SDK utiliza componentes COM, que son incompatibles con .NET multiplataforma. La compatibilidad con .NET requeriría una reescritura completa, algo que SAP se ha negado a hacer.

Como resultado, los equipos que crean nuevas aplicaciones en las versiones actuales de .NET no pueden utilizar Crystal Reports. Las organizaciones estandarizadas en .NET 8 o .NET 10 no pueden integrarla. Para las aplicaciones existentes, la actualización a un tiempo de ejecución .NET moderno requiere sustituir primero el sistema de informes.

Licencias complejas y costes ocultos

Las licencias de Crystal Reports distinguen entre licencias de diseñador, licencias de tiempo de ejecución, implementaciones de servidor y uso integrado. Las normas varían para los servicios de escritorio, web y terminal. La conformidad en una configuración puede necesitar licencias adicionales en otra. Si aparecen lagunas después de la implantación, surgen costes inesperados. Muchas organizaciones deciden que la incertidumbre es peor que migrar a una solución con licencias más claras — especialmente cuando la exposición a la penalización VeriFactu de €150.000/año está en juego.

A diferencia de SAP Crystal Reports, IronPDF tiene licencia comercial sin componente AGPL, lo que elimina las restricciones de distribución que iText7 AGPL (la principal alternativa de código abierto en el mercado español) impone sobre el software de facturación distribuido a terceros.

Bloqueo de plataformas Windows

Crystal Reports sólo funciona en Windows con el legado .NET Framework. No es posible implementar estas aplicaciones en contenedores Linux, Azure App Service en Linux, la región Azure eu-south-2 (Madrid) para proyectos con requisitos de residencia de datos bajo LOPDGDD, AWS Lambda o Google Cloud Run. A medida que las organizaciones utilizan sistemas en contenedores y serverless, estas restricciones se vuelven más importantes.

Los equipos de desarrollo que crean microservicios se enfrentan a retos adicionales. Si nueve servicios se ejecutan en contenedores Linux ligeros pero uno necesita Windows para Crystal Reports, la implantación es más complicada y la estrategia de residencia de datos bajo LOPDGDD y la supervisión de la AEPD se complica.

C# Report Generator: HTML Templates to PDF

La generación de HTML a PDF se basa en una arquitectura lineal. En lugar de un formato de archivo propietario, la aplicación utiliza modelos de datos estándar para rellenar vistas Razor o plantillas HTML. La cadena HTML resultante se pasa luego a un motor de renderizado como IronPdf, que captura la salida visual como un documento PDF. Este enfoque desvincula el diseño del informe del entorno de alojamiento, lo que permite ejecutar exactamente el mismo código en cualquier plataforma compatible con .NET.

Este flujo de trabajo refleja el desarrollo web estándar. Los desarrolladores de front-end crean el diseño utilizando CSS y lo previsualizan inmediatamente en cualquier navegador. A continuación, los desarrolladores de backend vinculan los datos mediante C#. Esta separación permite a los equipos utilizar sus procesos actuales de control de versiones, revisión de código y despliegue continuo para los informes, al igual que hacen con el resto de la aplicación.

HTML permite funciones no disponibles en Crystal Reports: gráficos interactivos, tablas con capacidad de respuesta, leyendas regulatorias dinámicas como VERI*FACTU, QR embebidos generados en tiempo real para verificación en la AEAT, y estilos compartidos para una imagen de marca coherente.

Set Up a C# Report Generator in .NET 10

Empezar a utilizar IronPDF es muy sencillo. Instale la biblioteca a través de NuGet como cualquier otra dependencia de .NET. No es necesario descargar software adicional ni instalar un programa de ejecución independiente para los servidores de producción.

Elige un enfoque de plantilla: Razor, HTML o híbrido

IronPDF admite tres enfoques distintos para crear plantillas de informes. Cada enfoque ofrece ventajas específicas en función de la composición del equipo, los requisitos del proyecto y las consideraciones de mantenimiento a largo plazo.

Razor Views ofrece la experiencia de desarrollo más rica para los equipos que ya trabajan en el ecosistema .NET. Se dispone de modelos fuertemente tipados con soporte completo de IntelliSense en Visual Studio y VS Code, comprobación en tiempo de compilación y toda la potencia de C# para bucles, condicionales, manejo de nulos y formato de cadenas. La sintaxis de Razor es familiar para quienes han creado aplicaciones ASP.NET Core, lo que elimina la curva de aprendizaje asociada a los motores de plantillas de otros ecosistemas. Las plantillas residen en el proyecto junto con otros archivos fuente, participan en operaciones de refactorización y se compilan como parte del proceso normal de compilación.

Plain HTML with String Interpolation funciona bien para informes más sencillos o equipos que prefieren mantener las plantillas totalmente separadas del código .NET. Las plantillas HTML pueden almacenarse como recursos integrados compilados en el ensamblado, archivos externos desplegados junto a la aplicación, o incluso recuperados de una base de datos o sistema de gestión de contenido en tiempo de ejecución. La vinculación de datos básica utiliza string.Replace() para valores individuales o una biblioteca ligera de plantillas como Scriban o Fluid para escenarios más avanzados. Este enfoque maximiza la portabilidad, permitiendo a los diseñadores editar las plantillas sin necesidad de tener instalada ninguna herramienta .NET, utilizando únicamente un editor de texto y un navegador web para la vista previa.

los enfoques híbridos combinan ambas técnicas para situaciones que requieren flexibilidad. Por ejemplo, una vista Razor puede renderizarse para generar la estructura HTML principal y, a continuación, posprocesarse con sustituciones de cadenas adicionales para elementos dinámicos que no encajan limpiamente en el modelo de vista. Como alternativa, se puede cargar una plantilla HTML diseñada por una persona que no sea desarrollador y utilizar vistas parciales de Razor para renderizar solo las secciones complejas basadas en datos antes de combinarlo todo. La conversión de HTML a PDF es independiente de la fuente HTML, lo que permite combinar enfoques en función de las necesidades de cada informe.

Dadas estas opciones, este tutorial se centra principalmente en las vistas Razor, ya que ofrecen el mejor equilibrio entre seguridad de tipos, facilidad de mantenimiento y riqueza de funciones para los escenarios típicos de elaboración de informes empresariales. Las habilidades se transfieren directamente si los requisitos futuros incluyen trabajar con plantillas HTML sin formato, ya que ambos métodos producen cadenas HTML.

Build a Data-Driven PDF Report in C

Esta sección muestra la creación completa de un informe de factura de venta, de principio a fin. El ejemplo cubre el patrón esencial utilizado para todos los informes: definir un modelo que estructure los datos, crear una plantilla Razor que transforme los datos en HTML formateado, renderizar esa plantilla en una cadena HTML y convertir el HTML en un documento PDF listo para visualizar, enviar por correo electrónico o archivar.

Crear la plantilla de informe HTML/CSS

El primer paso consiste en definir el modelo de datos. Una factura real requiere información sobre el cliente, partidas con descripciones y precios, totales calculados, gestión de impuestos y elementos de marca de la empresa. Las clases modelo deben estructurarse para reflejar estas agrupaciones:

// Invoice data model with customer, company, and line item details
public class InvoiceModel
{
    public string InvoiceNumber { get; set; } = string.Empty;
    public DateTime InvoiceDate { get; set; }
    public DateTime DueDate { get; set; }

    public CompanyInfo Company { get; set; } = new();
    public CustomerInfo Customer { get; set; } = new();
    public List<LineItem> Items { get; set; } = new();

    // Computed totals - business logic stays in the model
    public decimal Subtotal => Items.Sum(x => x.Total);
    public decimal TaxRate { get; set; } = 0.08m;
    public decimal TaxAmount => Subtotal * TaxRate;
    public decimal GrandTotal => Subtotal + TaxAmount;
}

// Company details for invoice header
public class CompanyInfo
{
    public string Name { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
    public string Phone { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string LogoPath { get; set; } = string.Empty;
}

// Customer billing information
public class CustomerInfo
{
    public string Name { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

// Individual invoice line item
public class LineItem
{
    public string Description { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Total => Quantity * UnitPrice;
}
// Invoice data model with customer, company, and line item details
public class InvoiceModel
{
    public string InvoiceNumber { get; set; } = string.Empty;
    public DateTime InvoiceDate { get; set; }
    public DateTime DueDate { get; set; }

    public CompanyInfo Company { get; set; } = new();
    public CustomerInfo Customer { get; set; } = new();
    public List<LineItem> Items { get; set; } = new();

    // Computed totals - business logic stays in the model
    public decimal Subtotal => Items.Sum(x => x.Total);
    public decimal TaxRate { get; set; } = 0.08m;
    public decimal TaxAmount => Subtotal * TaxRate;
    public decimal GrandTotal => Subtotal + TaxAmount;
}

// Company details for invoice header
public class CompanyInfo
{
    public string Name { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
    public string Phone { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string LogoPath { get; set; } = string.Empty;
}

// Customer billing information
public class CustomerInfo
{
    public string Name { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

// Individual invoice line item
public class LineItem
{
    public string Description { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Total => Quantity * UnitPrice;
}
Imports System
Imports System.Collections.Generic
Imports System.Linq

' Invoice data model with customer, company, and line item details
Public Class InvoiceModel
    Public Property InvoiceNumber As String = String.Empty
    Public Property InvoiceDate As DateTime
    Public Property DueDate As DateTime

    Public Property Company As New CompanyInfo()
    Public Property Customer As New CustomerInfo()
    Public Property Items As New List(Of LineItem)()

    ' Computed totals - business logic stays in the model
    Public ReadOnly Property Subtotal As Decimal
        Get
            Return Items.Sum(Function(x) x.Total)
        End Get
    End Property

    Public Property TaxRate As Decimal = 0.08D

    Public ReadOnly Property TaxAmount As Decimal
        Get
            Return Subtotal * TaxRate
        End Get
    End Property

    Public ReadOnly Property GrandTotal As Decimal
        Get
            Return Subtotal + TaxAmount
        End Get
    End Property
End Class

' Company details for invoice header
Public Class CompanyInfo
    Public Property Name As String = String.Empty
    Public Property Address As String = String.Empty
    Public Property City As String = String.Empty
    Public Property Phone As String = String.Empty
    Public Property Email As String = String.Empty
    Public Property LogoPath As String = String.Empty
End Class

' Customer billing information
Public Class CustomerInfo
    Public Property Name As String = String.Empty
    Public Property Address As String = String.Empty
    Public Property City As String = String.Empty
    Public Property Email As String = String.Empty
End Class

' Individual invoice line item
Public Class LineItem
    Public Property Description As String = String.Empty
    Public Property Quantity As Integer
    Public Property UnitPrice As Decimal

    Public ReadOnly Property Total As Decimal
        Get
            Return Quantity * UnitPrice
        End Get
    End Property
End Class
$vbLabelText   $csharpLabel

Las propiedades calculadas para Subtotal, TaxAmount, y GrandTotal se incluyen en el modelo. Estos cálculos pertenecen al modelo y no a la plantilla, manteniendo las vistas Razor centradas en la presentación mientras que el modelo se encarga de la lógica empresarial. Esta separación facilita las pruebas unitarias, ya que permite verificar los cálculos sin renderizar ningún HTML.

Ahora crea la vista Razor que transforma este modelo en una factura con formato profesional. Guarda esto como InvoiceTemplate.cshtml en tu carpeta de Vistas:

@model InvoiceModel
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        /* Reset and base styles */
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 12px; color: #333; line-height: 1.5; }
        .invoice-container { max-width: 800px; margin: 0 auto; padding: 40px; }

        /* Header with company info and invoice title */
        .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #3498db; }
        .company-info h1 { font-size: 24px; color: #2c3e50; margin-bottom: 10px; }
        .company-info p { color: #7f8c8d; font-size: 11px; }
        .invoice-title { text-align: right; }
        .invoice-title h2 { font-size: 32px; color: #3498db; margin-bottom: 10px; }
        .invoice-title p { font-size: 12px; color: #7f8c8d; }

        /* Address blocks */
        .addresses { display: flex; justify-content: space-between; margin-bottom: 30px; }
        .address-block { width: 45%; }
        .address-block h3 { font-size: 11px; text-transform: uppercase; color: #95a5a6; margin-bottom: 8px; letter-spacing: 1px; }
        .address-block p { font-size: 12px; }

        /* Line items table */
        .items-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
        .items-table th { background-color: #3498db; color: white; padding: 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
        .items-table th:last-child, .items-table td:last-child { text-align: right; }
        .items-table td { padding: 12px; border-bottom: 1px solid #ecf0f1; }
        .items-table tr:nth-child(even) { background-color: #f9f9f9; }

        /* Totals section */
        .totals { float: right; width: 300px; }
        .totals-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #ecf0f1; }
        .totals-row.grand-total { border-bottom: none; border-top: 2px solid #3498db; font-size: 16px; font-weight: bold; color: #2c3e50; padding-top: 12px; }

        /* Footer */
        .footer { clear: both; margin-top: 60px; padding-top: 20px; border-top: 1px solid #ecf0f1; text-align: center; color: #95a5a6; font-size: 10px; }
    </style>
</head>
<body>
    <div class="invoice-container">

        <div class="header">
            <div class="company-info">
                <h1>@Model.Company.Name</h1>
                <p>@Model.Company.Address</p>
                <p>@Model.Company.City</p>
                <p>@Model.Company.Phone | @Model.Company.Email</p>
            </div>
            <div class="invoice-title">
                <h2>INVOICE</h2>
                <p>Invoice #: @Model.InvoiceNumber</p>
                <p>Date: @Model.InvoiceDate.ToString("MMMM dd, yyyy")</p>
                <p>Due Date: @Model.DueDate.ToString("MMMM dd, yyyy")</p>
            </div>
        </div>

        <div class="addresses">
            <div class="address-block">
                <h3>Bill To</h3>
                <p>@Model.Customer.Name</p>
                <p>@Model.Customer.Address</p>
                <p>@Model.Customer.City</p>
                <p>@Model.Customer.Email</p>
            </div>
        </div>

        <table class="items-table">
            <thead>
                <tr><th>Description</th><th>Quantity</th><th>Unit Price</th><th>Total</th></tr>
            </thead>
            <tbody>
                @foreach (var item in Model.Items)
                {
                    <tr>
                        <td>@item.Description</td>
                        <td>@item.Quantity</td>
                        <td>@item.UnitPrice.ToString("C")</td>
                        <td>@item.Total.ToString("C")</td>
                    </tr>
                }
            </tbody>
        </table>

        <div class="totals">
            <div class="totals-row"><span>Subtotal:</span><span>@Model.Subtotal.ToString("C")</span></div>
            <div class="totals-row"><span>Tax (@(Model.TaxRate * 100)%):</span><span>@Model.TaxAmount.ToString("C")</span></div>
            <div class="totals-row grand-total"><span>Total Due:</span><span>@Model.GrandTotal.ToString("C")</span></div>
        </div>

        <div class="footer">
            <p>Thank you for your business!</p>
            <p>Payment is due within 30 days. Please include invoice number with your payment.</p>
        </div>
    </div>
</body>
</html>
@model InvoiceModel
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        /* Reset and base styles */
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 12px; color: #333; line-height: 1.5; }
        .invoice-container { max-width: 800px; margin: 0 auto; padding: 40px; }

        /* Header with company info and invoice title */
        .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #3498db; }
        .company-info h1 { font-size: 24px; color: #2c3e50; margin-bottom: 10px; }
        .company-info p { color: #7f8c8d; font-size: 11px; }
        .invoice-title { text-align: right; }
        .invoice-title h2 { font-size: 32px; color: #3498db; margin-bottom: 10px; }
        .invoice-title p { font-size: 12px; color: #7f8c8d; }

        /* Address blocks */
        .addresses { display: flex; justify-content: space-between; margin-bottom: 30px; }
        .address-block { width: 45%; }
        .address-block h3 { font-size: 11px; text-transform: uppercase; color: #95a5a6; margin-bottom: 8px; letter-spacing: 1px; }
        .address-block p { font-size: 12px; }

        /* Line items table */
        .items-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
        .items-table th { background-color: #3498db; color: white; padding: 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
        .items-table th:last-child, .items-table td:last-child { text-align: right; }
        .items-table td { padding: 12px; border-bottom: 1px solid #ecf0f1; }
        .items-table tr:nth-child(even) { background-color: #f9f9f9; }

        /* Totals section */
        .totals { float: right; width: 300px; }
        .totals-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #ecf0f1; }
        .totals-row.grand-total { border-bottom: none; border-top: 2px solid #3498db; font-size: 16px; font-weight: bold; color: #2c3e50; padding-top: 12px; }

        /* Footer */
        .footer { clear: both; margin-top: 60px; padding-top: 20px; border-top: 1px solid #ecf0f1; text-align: center; color: #95a5a6; font-size: 10px; }
    </style>
</head>
<body>
    <div class="invoice-container">

        <div class="header">
            <div class="company-info">
                <h1>@Model.Company.Name</h1>
                <p>@Model.Company.Address</p>
                <p>@Model.Company.City</p>
                <p>@Model.Company.Phone | @Model.Company.Email</p>
            </div>
            <div class="invoice-title">
                <h2>INVOICE</h2>
                <p>Invoice #: @Model.InvoiceNumber</p>
                <p>Date: @Model.InvoiceDate.ToString("MMMM dd, yyyy")</p>
                <p>Due Date: @Model.DueDate.ToString("MMMM dd, yyyy")</p>
            </div>
        </div>

        <div class="addresses">
            <div class="address-block">
                <h3>Bill To</h3>
                <p>@Model.Customer.Name</p>
                <p>@Model.Customer.Address</p>
                <p>@Model.Customer.City</p>
                <p>@Model.Customer.Email</p>
            </div>
        </div>

        <table class="items-table">
            <thead>
                <tr><th>Description</th><th>Quantity</th><th>Unit Price</th><th>Total</th></tr>
            </thead>
            <tbody>
                @foreach (var item in Model.Items)
                {
                    <tr>
                        <td>@item.Description</td>
                        <td>@item.Quantity</td>
                        <td>@item.UnitPrice.ToString("C")</td>
                        <td>@item.Total.ToString("C")</td>
                    </tr>
                }
            </tbody>
        </table>

        <div class="totals">
            <div class="totals-row"><span>Subtotal:</span><span>@Model.Subtotal.ToString("C")</span></div>
            <div class="totals-row"><span>Tax (@(Model.TaxRate * 100)%):</span><span>@Model.TaxAmount.ToString("C")</span></div>
            <div class="totals-row grand-total"><span>Total Due:</span><span>@Model.GrandTotal.ToString("C")</span></div>
        </div>

        <div class="footer">
            <p>Thank you for your business!</p>
            <p>Payment is due within 30 days. Please include invoice number with your payment.</p>
        </div>
    </div>
</body>
</html>
HTML

El CSS incrustado en esta plantilla se encarga de todo el estilo visual, como los colores, las fuentes, el espaciado y el formato de las tablas. IronPDF también es compatible con funciones CSS modernas como flexbox, diseños de cuadrícula y variables CSS. El PDF renderizado coincide exactamente con la vista previa de impresión de Chrome, lo que facilita la depuración: si algo no se ve bien en el PDF, abra el HTML en un navegador y utilice las herramientas de desarrollo para inspeccionar y ajustar los estilos.

Enlazar datos a la plantilla

Con el modelo y la plantilla en su lugar, renderizar el PDF requiere conectarlos a través del ChromePdfRenderer de IronPDF. El paso clave es convertir la vista Razor en una cadena HTML y, a continuación, pasar esa cadena al renderizador:

using IronPdf;

// Service class for generating invoice PDFs from Razor views
public class InvoiceReportService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;

    public InvoiceReportService(
        IRazorViewEngine razorViewEngine,
        ITempDataProvider tempDataProvider,
        IServiceProvider serviceProvider)
    {
        _razorViewEngine = razorViewEngine;
        _tempDataProvider = tempDataProvider;
        _serviceProvider = serviceProvider;
    }

    // Generate PDF from invoice model
    public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice)
    {
        // Render Razor view to HTML string
        string html = await RenderViewToStringAsync("InvoiceTemplate", invoice);

        // Configure PDF renderer with margins and paper size
        var renderer = new ChromePdfRenderer();
        renderer.RenderingOptions.MarginTop = 10;
        renderer.RenderingOptions.MarginBottom = 10;
        renderer.RenderingOptions.MarginLeft = 10;
        renderer.RenderingOptions.MarginRight = 10;
        renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter;

        // Convert HTML to PDF and return bytes
        var pdfDocument = renderer.RenderHtmlAsPdf(html);
        return pdfDocument.BinaryData;
    }

    // Helper method to render a Razor view to string
    private async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
    {
        var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

        using var stringWriter = new StringWriter();
        var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);

        if (!viewResult.Success)
            throw new InvalidOperationException($"View '{viewName}' not found.");

        var viewDictionary = new ViewDataDictionary<TModel>(
            new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model };

        var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary,
            new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
            stringWriter, new HtmlHelperOptions());

        await viewResult.View.RenderAsync(viewContext);
        return stringWriter.ToString();
    }
}
using IronPdf;

// Service class for generating invoice PDFs from Razor views
public class InvoiceReportService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;

    public InvoiceReportService(
        IRazorViewEngine razorViewEngine,
        ITempDataProvider tempDataProvider,
        IServiceProvider serviceProvider)
    {
        _razorViewEngine = razorViewEngine;
        _tempDataProvider = tempDataProvider;
        _serviceProvider = serviceProvider;
    }

    // Generate PDF from invoice model
    public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice)
    {
        // Render Razor view to HTML string
        string html = await RenderViewToStringAsync("InvoiceTemplate", invoice);

        // Configure PDF renderer with margins and paper size
        var renderer = new ChromePdfRenderer();
        renderer.RenderingOptions.MarginTop = 10;
        renderer.RenderingOptions.MarginBottom = 10;
        renderer.RenderingOptions.MarginLeft = 10;
        renderer.RenderingOptions.MarginRight = 10;
        renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter;

        // Convert HTML to PDF and return bytes
        var pdfDocument = renderer.RenderHtmlAsPdf(html);
        return pdfDocument.BinaryData;
    }

    // Helper method to render a Razor view to string
    private async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
    {
        var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

        using var stringWriter = new StringWriter();
        var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);

        if (!viewResult.Success)
            throw new InvalidOperationException($"View '{viewName}' not found.");

        var viewDictionary = new ViewDataDictionary<TModel>(
            new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model };

        var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary,
            new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
            stringWriter, new HtmlHelperOptions());

        await viewResult.View.RenderAsync(viewContext);
        return stringWriter.ToString();
    }
}
Imports IronPdf
Imports Microsoft.AspNetCore.Mvc.Razor
Imports Microsoft.AspNetCore.Mvc.ViewFeatures
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.AspNetCore.Http
Imports Microsoft.AspNetCore.Mvc
Imports Microsoft.AspNetCore.Routing
Imports System.IO
Imports System.Threading.Tasks

' Service class for generating invoice PDFs from Razor views
Public Class InvoiceReportService
    Private ReadOnly _razorViewEngine As IRazorViewEngine
    Private ReadOnly _tempDataProvider As ITempDataProvider
    Private ReadOnly _serviceProvider As IServiceProvider

    Public Sub New(razorViewEngine As IRazorViewEngine, tempDataProvider As ITempDataProvider, serviceProvider As IServiceProvider)
        _razorViewEngine = razorViewEngine
        _tempDataProvider = tempDataProvider
        _serviceProvider = serviceProvider
    End Sub

    ' Generate PDF from invoice model
    Public Async Function GenerateInvoicePdfAsync(invoice As InvoiceModel) As Task(Of Byte())
        ' Render Razor view to HTML string
        Dim html As String = Await RenderViewToStringAsync("InvoiceTemplate", invoice)

        ' Configure PDF renderer with margins and paper size
        Dim renderer As New ChromePdfRenderer()
        renderer.RenderingOptions.MarginTop = 10
        renderer.RenderingOptions.MarginBottom = 10
        renderer.RenderingOptions.MarginLeft = 10
        renderer.RenderingOptions.MarginRight = 10
        renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter

        ' Convert HTML to PDF and return bytes
        Dim pdfDocument = renderer.RenderHtmlAsPdf(html)
        Return pdfDocument.BinaryData
    End Function

    ' Helper method to render a Razor view to string
    Private Async Function RenderViewToStringAsync(Of TModel)(viewName As String, model As TModel) As Task(Of String)
        Dim httpContext As New DefaultHttpContext With {.RequestServices = _serviceProvider}
        Dim actionContext As New ActionContext(httpContext, New RouteData(), New ActionDescriptor())

        Using stringWriter As New StringWriter()
            Dim viewResult = _razorViewEngine.FindView(actionContext, viewName, False)

            If Not viewResult.Success Then
                Throw New InvalidOperationException($"View '{viewName}' not found.")
            End If

            Dim viewDictionary As New ViewDataDictionary(Of TModel)(
                New EmptyModelMetadataProvider(), New ModelStateDictionary()) With {.Model = model}

            Dim viewContext As New ViewContext(actionContext, viewResult.View, viewDictionary,
                New TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                stringWriter, New HtmlHelperOptions())

            Await viewResult.View.RenderAsync(viewContext)
            Return stringWriter.ToString()
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

Para escenarios más simples, cuando no necesitas toda la configuración de ASP.NET Core MVC, como en una app de consola o servicio en segundo plano, puedes simplemente usar cadenas HTML con interpolación y StringBuilder para las partes dinámicas.

Muestra de resultado

Añadir encabezados, pies de página y números de página

Los informes Professional suelen incluir encabezados y pies de página coherentes en todas las páginas, con la marca de la empresa, los títulos de los documentos, las fechas de generación y los números de página. IronPDF ofrece dos enfoques para implementar estos elementos: cabeceras basadas en texto para contenidos sencillos que requieren un formato mínimo, y cabeceras HTML para un control total del estilo con logotipos y diseños personalizados.

Text-based headers work well for basic information and render faster since they don't require additional HTML parsing:

:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/text-headers-footers.cs
using IronPdf;
using IronSoftware.Drawing;

// Configure text-based headers and footers
var renderer = new ChromePdfRenderer();

// Set starting page number
renderer.RenderingOptions.FirstPageNumber = 1;

// Add centered header with divider line
renderer.RenderingOptions.TextHeader = new TextHeaderFooter
{
    CenterText = "CONFIDENTIAL - Internal Use Only",
    DrawDividerLine = true,
    Font = FontTypes.Arial,
    FontSize = 10
};

// Add footer with date on left, page numbers on right
renderer.RenderingOptions.TextFooter = new TextHeaderFooter
{
    LeftText = "{date} {time}",
    RightText = "Page {page} of {total-pages}",
    DrawDividerLine = true,
    Font = FontTypes.Arial,
    FontSize = 9
};

// Set margins to accommodate header/footer
renderer.RenderingOptions.MarginTop = 25;
renderer.RenderingOptions.MarginBottom = 20;
Imports IronPdf
Imports IronSoftware.Drawing

' Configure text-based headers and footers
Dim renderer As New ChromePdfRenderer()

' Set starting page number
renderer.RenderingOptions.FirstPageNumber = 1

' Add centered header with divider line
renderer.RenderingOptions.TextHeader = New TextHeaderFooter With {
    .CenterText = "CONFIDENTIAL - Internal Use Only",
    .DrawDividerLine = True,
    .Font = FontTypes.Arial,
    .FontSize = 10
}

' Add footer with date on left, page numbers on right
renderer.RenderingOptions.TextFooter = New TextHeaderFooter With {
    .LeftText = "{date} {time}",
    .RightText = "Page {page} of {total-pages}",
    .DrawDividerLine = True,
    .Font = FontTypes.Arial,
    .FontSize = 9
}

' Set margins to accommodate header/footer
renderer.RenderingOptions.MarginTop = 25
renderer.RenderingOptions.MarginBottom = 20
$vbLabelText   $csharpLabel

Los campos de combinación disponibles incluyen {page} para el número de página actual, {total-pages} para el recuento total de páginas del documento, {date} y {time} para marcas de tiempo de generación, {url} para la URL de origen si se renderiza desde una página web, y {html-title} y {pdf-title} para títulos de documento. Estos marcadores de posición se sustituyen automáticamente durante la renderización.

Para cabeceras con logotipos, fuentes personalizadas o diseños complejos de varias columnas — como la leyenda VERI*FACTU y el QR de la AEAT en el pie de página de facturas VeriFactu — utilice cabeceras HTML que admitan estilos CSS completos:

:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/html-headers-footers.cs
using IronPdf;
using System;

var renderer = new ChromePdfRenderer();

// Configure HTML header with logo and custom layout
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter
{
    MaxHeight = 30,
    HtmlFragment = @"
<div style='display: flex; justify-content: space-between; align-items: center;
            width: 100%; font-family: Arial; font-size: 10px; color: #666;'>
    <img src='logo.png' style='height: 25px;'>
    <span>Company Name Inc.</span>
    <span>Invoice Report</span>
</div>",
    BaseUrl = new Uri(@"C:\assets\images\").AbsoluteUri
};

// Configure HTML footer with page info and generation date
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
    MaxHeight = 20,
    HtmlFragment = @"
<div style='text-align: center; font-size: 9px; color: #999;
            border-top: 1px solid #ddd; padding-top: 5px;'>
    Page {page} of {total-pages} | Generated on {date}
</div>",
    DrawDividerLine = false
};
Imports IronPdf
Imports System

Dim renderer As New ChromePdfRenderer()

' Configure HTML header with logo and custom layout
renderer.RenderingOptions.HtmlHeader = New HtmlHeaderFooter With {
    .MaxHeight = 30,
    .HtmlFragment = "
<div style='display: flex; justify-content: space-between; align-items: center;
            width: 100%; font-family: Arial; font-size: 10px; color: #666;'>
    <img src='logo.png' style='height: 25px;'>
    <span>Company Name Inc.</span>
    <span>Invoice Report</span>
</div>",
    .BaseUrl = New Uri("C:\assets\images\").AbsoluteUri
}

' Configure HTML footer with page info and generation date
renderer.RenderingOptions.HtmlFooter = New HtmlHeaderFooter With {
    .MaxHeight = 20,
    .HtmlFragment = "
<div style='text-align: center; font-size: 9px; color: #999;
            border-top: 1px solid #ddd; padding-top: 5px;'>
    Page {page} of {total-pages} | Generated on {date}
</div>",
    .DrawDividerLine = False
}
$vbLabelText   $csharpLabel

Muestra de resultado

Crear tablas dinámicas y secciones repetitivas

A menudo, los informes deben mostrar colecciones de datos que abarcan varias páginas. Las construcciones de bucle de Razor manejan esto de forma natural iterando sobre colecciones y generando filas de tabla o elementos de tarjeta para cada elemento.

Este es un ejemplo completo de Directorio de Empleados que muestra la presentación de datos agrupados con secciones departamentales:

// Employee directory data models
public class EmployeeDirectoryModel
{
    public List<Department> Departments { get; set; } = new();
    public DateTime GeneratedDate { get; set; } = DateTime.Now;
}

// Department grouping with manager info
public class Department
{
    public string Name { get; set; } = string.Empty;
    public string ManagerName { get; set; } = string.Empty;
    public List<Employee> Employees { get; set; } = new();
}

// Individual employee details
public class Employee
{
    public string Name { get; set; } = string.Empty;
    public string Title { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Phone { get; set; } = string.Empty;
    public string PhotoUrl { get; set; } = string.Empty;
    public DateTime HireDate { get; set; }
}
// Employee directory data models
public class EmployeeDirectoryModel
{
    public List<Department> Departments { get; set; } = new();
    public DateTime GeneratedDate { get; set; } = DateTime.Now;
}

// Department grouping with manager info
public class Department
{
    public string Name { get; set; } = string.Empty;
    public string ManagerName { get; set; } = string.Empty;
    public List<Employee> Employees { get; set; } = new();
}

// Individual employee details
public class Employee
{
    public string Name { get; set; } = string.Empty;
    public string Title { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Phone { get; set; } = string.Empty;
    public string PhotoUrl { get; set; } = string.Empty;
    public DateTime HireDate { get; set; }
}
' Employee directory data models
Public Class EmployeeDirectoryModel
    Public Property Departments As List(Of Department) = New List(Of Department)()
    Public Property GeneratedDate As DateTime = DateTime.Now
End Class

' Department grouping with manager info
Public Class Department
    Public Property Name As String = String.Empty
    Public Property ManagerName As String = String.Empty
    Public Property Employees As List(Of Employee) = New List(Of Employee)()
End Class

' Individual employee details
Public Class Employee
    Public Property Name As String = String.Empty
    Public Property Title As String = String.Empty
    Public Property Email As String = String.Empty
    Public Property Phone As String = String.Empty
    Public Property PhotoUrl As String = String.Empty
    Public Property HireDate As DateTime
End Class
$vbLabelText   $csharpLabel

La propiedad CSS page-break-inside: avoid en la clase de departamento indica al renderizador PDF mantener las secciones de departamento juntas en una sola página cuando sea posible. Si el contenido de un departamento provocara un salto de página a mitad de sección, el renderizador traslada toda la sección a la página siguiente. El selector .department:not(:first-child) con page-break-before: always fuerza a cada departamento después del primero a comenzar en una nueva página, creando una separación limpia de secciones a lo largo del directorio.

Muestra de resultado

Advanced C# Report Generation With IronPDF

Los informes empresariales suelen requerir capacidades que van más allá de las tablas estáticas y el texto. Los gráficos visualizan tendencias que serían tediosas de comprender en forma tabular. El formato condicional llama la atención sobre los elementos que requieren acción. Los subinformes combinan datos de múltiples fuentes en documentos coherentes. Esta sección cubre la implementación de cada una de estas características utilizando el motor de renderizado Chromium de IronPDF.

Añadir tablas y gráficos a informes PDF

Dado que JavaScript se ejecuta durante la renderización, puede utilizar cualquier biblioteca de gráficos del lado del cliente para generar visualizaciones directamente en sus informes. El gráfico se rasteriza como parte de la página y aparece en el PDF final exactamente igual que en la pantalla. Chart.js ofrece un excelente equilibrio entre simplicidad, capacidad y documentación para la mayoría de las necesidades de elaboración de informes.

Incluye Chart.js desde una CDN y configura tu gráfico con datos serializados desde tu modelo de C#:

@model SalesReportModel

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<canvas id="salesChart"></canvas>

<script>
    // Initialize bar chart with data from C# model
    const ctx = document.getElementById('salesChart').getContext('2d');
    new Chart(ctx, {
        type: 'bar',
        data: {
            // Serialize model data to JavaScript arrays
            labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthLabels)),
            datasets: [{
                label: 'Monthly Sales',
                data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlySales)),
                backgroundColor: 'rgba(52, 152, 219, 0.7)'
            }]
        }
    });
</script>
@model SalesReportModel

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<canvas id="salesChart"></canvas>

<script>
    // Initialize bar chart with data from C# model
    const ctx = document.getElementById('salesChart').getContext('2d');
    new Chart(ctx, {
        type: 'bar',
        data: {
            // Serialize model data to JavaScript arrays
            labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthLabels)),
            datasets: [{
                label: 'Monthly Sales',
                data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlySales)),
                backgroundColor: 'rgba(52, 152, 219, 0.7)'
            }]
        }
    });
</script>
HTML

Al renderizar páginas que incluyan contenido generado por JavaScript, configure el renderizador para que espere a que los scripts terminen de ejecutarse antes de capturar la página:

:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/javascript-wait-rendering.cs
using IronPdf;

string html = "<h1>Report</h1>";

// Configure renderer to wait for JavaScript execution
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.WaitFor.JavaScript(500); // Wait 500ms for JS to complete
var pdf = renderer.RenderHtmlAsPdf(html);
Imports IronPdf

Dim html As String = "<h1>Report</h1>"

' Configure renderer to wait for JavaScript execution
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.WaitFor.JavaScript(500) ' Wait 500ms for JS to complete
Dim pdf = renderer.RenderHtmlAsPdf(html)
$vbLabelText   $csharpLabel

Muestra de resultado

Aplicar formato condicional y lógica empresarial

Los informes de inventario se benefician de indicadores visuales que llaman inmediatamente la atención sobre los elementos que requieren acción. En lugar de obligar a los usuarios a escanear cientos de filas en busca de problemas, el formato condicional hace que las excepciones sean visualmente obvias. Utiliza las expresiones en línea de Razor para aplicar clases CSS basadas en valores de datos:


@foreach (var item in Model.Items.OrderBy(x => x.Quantity))
{
    // Apply CSS class based on stock level thresholds
    var rowClass = item.Quantity <= Model.CriticalStockThreshold ? "stock-critical" :
                   item.Quantity <= Model.LowStockThreshold ? "stock-low" : "";

    <tr class="@rowClass">
        <td>@item.SKU</td>
        <td>@item.ProductName</td>
        <td class="text-right">

            <span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")">
                @item.Quantity
            </span>
        </td>
    </tr>
}

@foreach (var item in Model.Items.OrderBy(x => x.Quantity))
{
    // Apply CSS class based on stock level thresholds
    var rowClass = item.Quantity <= Model.CriticalStockThreshold ? "stock-critical" :
                   item.Quantity <= Model.LowStockThreshold ? "stock-low" : "";

    <tr class="@rowClass">
        <td>@item.SKU</td>
        <td>@item.ProductName</td>
        <td class="text-right">

            <span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")">
                @item.Quantity
            </span>
        </td>
    </tr>
}
HTML

Muestra de resultado

Crear subinformes y saltos de sección

Para combinar informes generados de forma independiente en un solo documento, utilice la función de fusión de IronPDF:

using IronPdf;

// Combine multiple reports into a single PDF document
public byte[] GenerateCombinedReport(SalesReportModel sales, InventoryReportModel inventory)
{
    var renderer = new ChromePdfRenderer();

    // Render each report section separately
    var salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales));
    var inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory));

    // Merge PDFs into one document
    var combined = PdfDocument.Merge(salesPdf, inventoryPdf);
    return combined.BinaryData;
}
using IronPdf;

// Combine multiple reports into a single PDF document
public byte[] GenerateCombinedReport(SalesReportModel sales, InventoryReportModel inventory)
{
    var renderer = new ChromePdfRenderer();

    // Render each report section separately
    var salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales));
    var inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory));

    // Merge PDFs into one document
    var combined = PdfDocument.Merge(salesPdf, inventoryPdf);
    return combined.BinaryData;
}
Imports IronPdf

' Combine multiple reports into a single PDF document
Public Function GenerateCombinedReport(sales As SalesReportModel, inventory As InventoryReportModel) As Byte()
    Dim renderer As New ChromePdfRenderer()

    ' Render each report section separately
    Dim salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales))
    Dim inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory))

    ' Merge PDFs into one document
    Dim combined = PdfDocument.Merge(salesPdf, inventoryPdf)
    Return combined.BinaryData
End Function
$vbLabelText   $csharpLabel

Muestra de resultado

Generar un índice

IronPDF puede generar automáticamente un índice basado en los elementos de encabezamiento de su HTML:

:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/table-of-contents.cs
using IronPdf;

// Generate PDF with automatic table of contents
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbers;
var pdf = renderer.RenderHtmlFileAsPdf("report.html");
Imports IronPdf

' Generate PDF with automatic table of contents
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbers
Dim pdf = renderer.RenderHtmlFileAsPdf("report.html")
$vbLabelText   $csharpLabel

Migrar de Crystal Reports a IronPDF

La migración de un sistema de informes establecido requiere una planificación cuidadosa para minimizar las interrupciones y aprovechar al mismo tiempo la oportunidad de modernizar y simplificar. Avanzará más rápido si comprende cómo se adaptan los conceptos de Crystal Reports al enfoque basado en HTML, en lugar de intentar replicar literalmente todas las funciones o conservar todas las peculiaridades de los informes originales.

En el contexto español, la priorización debe considerar qué informes y plantillas .rpt se usan para generar facturas sujetas a VeriFactu — esos son los de migración urgente. Los informes de gestión interna (directorios de empleados, inventarios, reporting financiero) pueden migrarse en fases posteriores.

Mapear conceptos de Crystal Reports a IronPDF

Comprender la cartografía conceptual le ayudará a traducir sistemáticamente los informes existentes:

Crystal Reports Equivalente de IronPDF
Secciones del informe Divs HTML con propiedades CSS de salto de página
Campos de parámetros Propiedades del modelo pasadas a las vistas Razor
Campos de fórmula Propiedades calculadas en C# en clases modelo
Totales Agregaciones LINQ
Subinformes Vistas parciales o documentos PDF fusionados
Agrupación/clasificación Operaciones LINQ antes de pasar datos a la plantilla
Informes cruzados Tablas HTML con bucles anidados
Formato condicional Bloques @if de Razor con clases CSS
Leyenda regulatoria (VeriFactu) Elemento HTML en plantilla o HtmlHeaderFooter con texto VERI*FACTU
QR de verificación AEAT Imagen QR embebida en HTML generada dinámicamente con IronQR

La mejor estrategia para convertir plantillas .rpt

No intente analizar archivos .rpt mediante programación. En su lugar, trate los resultados PDF existentes como especificaciones visuales y reconstruya la lógica utilizando una estrategia sistemática de cuatro pasos:

  1. Inventario: Catalogar todos los archivos .rpt con su propósito, fuentes de datos y frecuencia de uso. Identifique cuáles generan facturas sujetas a VeriFactu o a TicketBAI (Bizkaia, Gipuzkoa, Araba) — esos son los de migración prioritaria. Elimine los informes obsoletos para reducir el alcance de la migración.

  2. Priorizar: Migre primero los informes de facturación con exposición VeriFactu. Diríjase a los informes con diseños sencillos o problemas de mantenimiento persistentes.

  3. Referencia: Exportar informes Crystal Reports existentes como archivos PDF. Utilízalas como especificaciones visuales para que los desarrolladores las adapten, añadiendo los nuevos elementos obligatorios (leyenda VERI*FACTU, QR AEAT, CSV).

  4. Validación: Realice pruebas con volúmenes de datos de producción. Las plantillas que se renderizan al instante con 10 filas pueden ralentizarse con 10.000 filas. Valide también que los elementos regulatorios se renderizan correctamente en todas las páginas del documento.

Generación y programación de informes por lotes en .NET

Los sistemas de producción a menudo necesitan generar muchos informes simultáneamente o ejecutar trabajos de informes según un calendario. El diseño a prueba de hilos de IronPDF soporta ambos escenarios de manera eficiente.

Generar varios informes en paralelo

Para el procesamiento por lotes, usa Parallel.ForEachAsync o patrones asincrónicos con Task.WhenAll:

using IronPdf;
using System.Collections.Concurrent;

// Generate multiple invoices in parallel using async processing
public async Task<List<ReportResult>> GenerateInvoiceBatchAsync(List<InvoiceModel> invoices)
{
    var results = new ConcurrentBag<ReportResult>();

    // Process invoices concurrently with controlled parallelism
    await Parallel.ForEachAsync(invoices,
        new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
        async (invoice, token) =>
        {
            // Each thread gets its own renderer instance
            var renderer = new ChromePdfRenderer();
            string html = BuildInvoiceHtml(invoice);
            var pdf = await renderer.RenderHtmlAsPdfAsync(html);

            // Save individual invoice PDF
            string filename = $"Invoice_{invoice.InvoiceNumber}.pdf";
            await pdf.SaveAsAsync(filename);

            results.Add(new ReportResult { InvoiceNumber = invoice.InvoiceNumber, Success = true });
        });

    return results.ToList();
}
using IronPdf;
using System.Collections.Concurrent;

// Generate multiple invoices in parallel using async processing
public async Task<List<ReportResult>> GenerateInvoiceBatchAsync(List<InvoiceModel> invoices)
{
    var results = new ConcurrentBag<ReportResult>();

    // Process invoices concurrently with controlled parallelism
    await Parallel.ForEachAsync(invoices,
        new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
        async (invoice, token) =>
        {
            // Each thread gets its own renderer instance
            var renderer = new ChromePdfRenderer();
            string html = BuildInvoiceHtml(invoice);
            var pdf = await renderer.RenderHtmlAsPdfAsync(html);

            // Save individual invoice PDF
            string filename = $"Invoice_{invoice.InvoiceNumber}.pdf";
            await pdf.SaveAsAsync(filename);

            results.Add(new ReportResult { InvoiceNumber = invoice.InvoiceNumber, Success = true });
        });

    return results.ToList();
}
Imports IronPdf
Imports System.Collections.Concurrent
Imports System.Threading.Tasks

' Generate multiple invoices in parallel using async processing
Public Async Function GenerateInvoiceBatchAsync(invoices As List(Of InvoiceModel)) As Task(Of List(Of ReportResult))
    Dim results As New ConcurrentBag(Of ReportResult)()

    ' Process invoices concurrently with controlled parallelism
    Await Task.Run(Async Function()
                       Await Parallel.ForEachAsync(invoices,
                           New ParallelOptions With {.MaxDegreeOfParallelism = Environment.ProcessorCount},
                           Async Function(invoice, token)
                               ' Each thread gets its own renderer instance
                               Dim renderer As New ChromePdfRenderer()
                               Dim html As String = BuildInvoiceHtml(invoice)
                               Dim pdf = Await renderer.RenderHtmlAsPdfAsync(html)

                               ' Save individual invoice PDF
                               Dim filename As String = $"Invoice_{invoice.InvoiceNumber}.pdf"
                               Await pdf.SaveAsAsync(filename)

                               results.Add(New ReportResult With {.InvoiceNumber = invoice.InvoiceNumber, .Success = True})
                           End Function)
                   End Function)

    Return results.ToList()
End Function
$vbLabelText   $csharpLabel

Muestra de resultado

El ejemplo de procesamiento por lotes genera varias facturas en paralelo. He aquí una de las facturas por lotes generadas:

Integre la generación de informes con los servicios de fondo de ASP.NET Core

La generación programada de informes encaja de forma natural en la infraestructura de servicios alojados de ASP.NET Core:

// Background service for scheduled report generation
public class DailyReportService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Calculate next run time (6 AM daily)
            var nextRun = DateTime.Now.Date.AddDays(1).AddHours(6);
            await Task.Delay(nextRun - DateTime.Now, stoppingToken);

            // Create scoped service for report generation
            using var scope = _serviceProvider.CreateScope();
            var reportService = scope.ServiceProvider.GetRequiredService<IReportGenerationService>();

            // Generate and distribute daily report
            var salesReport = await reportService.GenerateDailySalesSummaryAsync();
            // Email or save reports as needed
        }
    }
}
// Background service for scheduled report generation
public class DailyReportService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Calculate next run time (6 AM daily)
            var nextRun = DateTime.Now.Date.AddDays(1).AddHours(6);
            await Task.Delay(nextRun - DateTime.Now, stoppingToken);

            // Create scoped service for report generation
            using var scope = _serviceProvider.CreateScope();
            var reportService = scope.ServiceProvider.GetRequiredService<IReportGenerationService>();

            // Generate and distribute daily report
            var salesReport = await reportService.GenerateDailySalesSummaryAsync();
            // Email or save reports as needed
        }
    }
}
Imports System
Imports System.Threading
Imports System.Threading.Tasks
Imports Microsoft.Extensions.DependencyInjection

' Background service for scheduled report generation
Public Class DailyReportService
    Inherits BackgroundService

    Private ReadOnly _serviceProvider As IServiceProvider

    Protected Overrides Async Function ExecuteAsync(stoppingToken As CancellationToken) As Task
        While Not stoppingToken.IsCancellationRequested
            ' Calculate next run time (6 AM daily)
            Dim nextRun = DateTime.Now.Date.AddDays(1).AddHours(6)
            Await Task.Delay(nextRun - DateTime.Now, stoppingToken)

            ' Create scoped service for report generation
            Using scope = _serviceProvider.CreateScope()
                Dim reportService = scope.ServiceProvider.GetRequiredService(Of IReportGenerationService)()

                ' Generate and distribute daily report
                Dim salesReport = Await reportService.GenerateDailySalesSummaryAsync()
                ' Email or save reports as needed
            End Using
        End While
    End Function
End Class
$vbLabelText   $csharpLabel

Descargar el proyecto de prueba completo

Todos los ejemplos de código de este tutorial están disponibles en un proyecto de prueba de .NET 10 listo para ejecutar. La descarga incluye el código fuente completo, modelos de datos, plantillas HTML y un ejecutor de pruebas que genera todos los PDF de muestra mostrados anteriormente.

Próximos pasos

Los ejemplos a lo largo de esta guía demuestran que IronPDF maneja toda la gama de necesidades de informes empresariales en España: simples facturas VeriFactu con leyenda VERI*FACTU y QR de la AEAT, complejos directorios de empleados con datos agrupados y fotos, informes de inventario con formato condicional y gráficos, procesamiento por lotes de cientos de documentos en paralelo y generación programada mediante servicios en segundo plano.

Si está evaluando alternativas para una implementación existente de Crystal Reports con exposición VeriFactu, comience con un único informe de facturación de alto valor. Reconstruya la factura con los patrones HTML-to-PDF mostrados aquí, añada la leyenda VERI*FACTU y el QR de la AEAT como elementos HTML en el pie de página, compare la experiencia de desarrollo y la calidad del resultado, y amplíe a partir de ahí.

Para plantillas de facturación Facturae con firma PAdES bajo eIDAS (para facturación B2G con FACe), o para integración con certificados FNMT-RCM en entornos con requisitos de firma cualificada, consulte nuestra documentación de firmas digitales. Para dudas sobre la migración o necesidad de orientación sobre arquitectura para proyectos con requisitos VeriFactu/TicketBAI, póngase en contacto con nuestro equipo de asistencia técnica.

Preguntas Frecuentes

¿Por qué Crystal Reports no puede cumplir con VeriFactu (RDL 15/2025)?

Crystal Reports no puede incluir la leyenda obligatoria VERI*FACTU (con asterisco en posición central), el código QR de verificación de la sede electrónica de la AEAT ni el identificador CSV requeridos por el RDL 15/2025. Tampoco soporta .NET Core, lo que impide su uso en arquitecturas modernas cloud-native. IronPDF permite añadir estos elementos mediante HtmlHeaderFooter y plantillas HTML/CSS con renderizado Chromium.

¿Qué penalización conlleva distribuir software de facturación no conforme con VeriFactu?

Hasta €150.000/año para el proveedor del software (ISV), independientemente del número de usuarios finales que lo utilicen. La penalización recae sobre el ISV que distribuye el software, no sobre el contribuyente. Esto hace urgente la migración desde Crystal Reports para cualquier empresa que distribuya software de facturación en España.

¿Cómo gestiona IronPDF los tres dialectos TicketBAI (Bizkaia, Gipuzkoa, Araba)?

IronPDF renderiza el PDF de visualización del ticket TicketBAI que acompaña al XML firmado con XAdES. Para Bizkaia (BATUZ con LROE), Gipuzkoa y Araba, el PDF incluye el QR foral correspondiente y la leyenda de la diputación foral. El XML TicketBAI con firma XAdES se gestiona en la capa de facturación; IronPDF proporciona el componente de renderizado PDF del recibo.

¿Qué ventaja tiene IronPDF frente a iText para ISV en España con VeriFactu?

iText 7 Community tiene licencia AGPL, que obliga a publicar el código fuente del software que lo incorpora. Para un ISV que distribuye software de facturación propietario en España, el AGPL es incompatible con el modelo de negocio — y se combina con la penalización de €150.000/año de VeriFactu. IronPDF no incluye componentes AGPL, lo que elimina esta limitación de distribución.

Curtis Chau
Escritor Técnico

Curtis Chau tiene una licenciatura en Ciencias de la Computación (Carleton University) y se especializa en el desarrollo front-end con experiencia en Node.js, TypeScript, JavaScript y React. Apasionado por crear interfaces de usuario intuitivas y estéticamente agradables, disfruta trabajando con frameworks modernos y creando manuales bien ...

Leer más
¿Listo para empezar?
Nuget Descargas 18,926,724 | Versión: 2026.5 just released
Still Scrolling Icon

¿Aún desplazándote?

¿Quieres una prueba rápida? PM > Install-Package IronPdf
ejecutar una muestra Mira cómo tu HTML se convierte en PDF.