Sin dependencias de vendors
100% open-source y MIT. No hay licencias, suscripciones ni lock-in con ningún proveedor.
Esta guía te ayuda a migrar a UruFactura desde diferentes puntos de partida, ya sea que estés usando una solución propietaria, integrando SOAP directamente, o usando otra librería.
Sin dependencias de vendors
100% open-source y MIT. No hay licencias, suscripciones ni lock-in con ningún proveedor.
API idiomática en C#
Objetos tipados, métodos async/await, DI-friendly. No más SOAP ni XML manual.
Mantenido con .NET 10
Soporte para las últimas versiones de .NET. Sin librerías legacy ni WCF.
Todo incluido
XML, firma digital, SOAP, numeración con CAE y reporte diario en un paquete; PDF y base de datos como complementos.
Si actualmente construís los XML y llamadas SOAP a mano:
// ❌ Antes: construir XML manualmente y llamar SOAPvar xml = $@"<?xml version=""1.0"" encoding=""UTF-8""?><CFE xmlns=""http://www.dgi.gub.uy/wdgi/CFEv1""> <eTck> <TmstFirma>{DateTime.UtcNow:o}</TmstFirma> <Encabezado> <IdDoc> <TipoCFE>101</TipoCFE> <Serie>A</Serie> <Nro>1</Nro> ... </IdDoc> </Encabezado> ... </eTck></CFE>";
// Firmar XML con XmlDsig... (código extenso)var soapEnvelope = WrapInSoapEnvelope(signedXml);var response = await httpClient.PostAsync(dgiEndpoint, soapContent);// Parsear respuesta SOAP manualmente...// ✅ Después: describís el comprobante; UruFactura lo valida, numera, firma y envíavar eticket = await client.EmitirAsync(new Cfe(TipoCfe.ETicket){ Detalle = [new LineaDetalle { NombreItem = "Servicio", Cantidad = 1, PrecioUnitario = 1000m }],});
var acuse = await client.EnviarAsync(eticket);Si estás migrando desde un módulo de facturación electrónica de un ERP (Biscuit, Pragma, ContaPyme, etc.):
| Tu sistema actual | UruFactura |
|---|---|
| Comprobante / Documento | Cfe para describirlo (un solo modelo: new Cfe(TipoCfe.ETicket), …) y CfeEmitido una vez emitido |
| Línea de artículo / ítem | LineaDetalle |
| Proveedor / Receptor | Receptor |
| Rango CAE / Talonario | Cae con RangoDesde / RangoHasta |
| Acuse del envío | AcuseSobre.Recibido + AcuseSobre.Estado (AS = sobre recibido) |
| Estado del comprobante | ConsultarEnvioAsync → CfeEmitido.AceptadoPorDgi (AE = aceptado) |
| Representación impresa | CfePdf.GenerarA4(emitido) / GenerarTermico(emitido) (paquete UruFactura.Pdf) |
Mantener ambos sistemas en paralelo
No apagues el sistema anterior hasta validar el nuevo en homologación.
Mapear los tipos de CFE
// Mapeo desde tu sistema a TipoCfeTipoCfe MapearTipo(string tipoSistemaAnterior) => tipoSistemaAnterior switch{ "TICKET" => TipoCfe.ETicket, "FACTURA" => TipoCfe.EFactura, "NC_TICKET" => TipoCfe.NotaCreditoETicket, "NC_FACTURA" => TipoCfe.NotaCreditoEFactura, _ => throw new ArgumentException($"Tipo desconocido: {tipoSistemaAnterior}")};Adaptar la estructura de datos
// Convertir entidad de tu sistema a LineaDetallestatic LineaDetalle ConvertirLinea(MiLineaFactura linea) => new LineaDetalle { NombreItem = linea.DescripcionProducto, Cantidad = linea.Cantidad, PrecioUnitario = linea.PrecioUnitario, IndFactIva = linea.EsExento ? TipoIva.Exento : linea.IvaMinimo ? TipoIva.Minimo : TipoIva.Basico, };Implementar una capa de servicio
public class FacturacionService(IUruFacturaClient client){ public async Task<ResultadoEmision> EmitirAsync(ComprobanteDTO dto) { var cfe = new Cfe(MapearTipo(dto.Tipo)) { NroInterno = dto.Id, // un reintento no lo emite dos veces (CfeDuplicadoException) Receptor = dto.Receptor is null ? null : new Receptor { Documento = dto.Receptor.Rut, RazonSocial = dto.Receptor.Nombre, Direccion = dto.Receptor.Direccion, Ciudad = dto.Receptor.Ciudad, }, Detalle = [.. dto.Lineas.Select(ConvertirLinea)], };
var emitido = await client.EmitirAsync(cfe); // valida, numera, firma y registra
// 1) Acuse del sobre (AS = recibido); 2) resultado del comprobante (AE = aceptado) var acuse = await client.EnviarAsync(emitido); if (acuse.Recibido) await client.ConsultarEnvioAsync(acuse);
return new ResultadoEmision { Numero = $"{emitido.Serie}-{emitido.Numero}", Estado = emitido.EstadoDgi?.ToString() ?? acuse.Estado.ToString(), Mensaje = emitido.MensajeDgi ?? acuse.Mensaje, PdfA4 = CfePdf.GenerarA4(emitido), }; }}Probar en homologación
Reproducí los escenarios más comunes de tu operación: ventas simples, notas de crédito, exportación.
Migrar los CAEs
Solicitá nuevos CAEs en DGI o trasladá los existentes. Registralos con:
await client.RegistrarCaeAsync(new Cae { NumeroAutorizacion = "…", TipoCfe = TipoCfe.ETicket, Serie = "A", RangoDesde = 1, RangoHasta = 1000, FechaVencimiento = … });Cambiar a producción
Ambiente = Ambiente.ProduccionSi estás usando otra librería .NET de facturación electrónica uruguaya:
| Operación | Librería genérica | UruFactura |
|---|---|---|
| Crear comprobante | new Comprobante(tipo) |
new Cfe(TipoCfe.ETicket) { … } |
| Agregar ítems | comprobante.AddItem(...) |
Detalle = [new LineaDetalle { … }, …] |
| Numerar y firmar | cae.Next(), firma.Sign(xml, cert) |
await client.EmitirAsync(cfe) |
| Enviar a DGI | soap.Send(xml) |
await client.EnviarAsync(emitido) |
| Generar PDF | pdf.Render(comprobante) |
CfePdf.GenerarA4(emitido) |
| Estado CFE | soap.ConsultarEstado(id) |
await client.ConsultarEnvioAsync(acuse) / ConsultarCfeAsync(emitido) |
// UruFactura — respuestas tipadasvar acuse = await client.EnviarAsync(emitido);
// acuse.Recibido: bool (AS)// acuse.Estado: EstadoSobre (Recibido = AS, Rechazado = BS, ArchivoRechazado = BA — ver Códigos DGI)// acuse.Mensaje / acuse.Motivos: descripción y motivos de la DGI
if (acuse.Recibido){ var estado = await client.ConsultarEnvioAsync(acuse); // emitido.AceptadoPorDgi, emitido.EstadoDgi (EstadoCfe)}// Los errores de comunicación (red, SOAP Fault, HTTP) se lanzan como DgiCommunicationExceptionAntes de ir a producción, verificá: