Muchas veces se me ocurrio intentar conectar mi aplicacion de gestion de empresa con el servicio electronico de AFIP (Ente regulador de la argentino) para por ejemplo realizar la facturacion electronica.
Resumen, es una mala idea. Este es mi segundo intento de realizar la conexion. En el primer intento fracase abruptamente por falta de documentacion, ejemplos que no funcionaban y otras cosas que no ayudaron a completar el proyecto con exito.
Esta vez en cambio, encontre un poco mas de ayuda en linea, estudie un poco mejor la legislacion y la documentacion del sistema, con lo que pude completar con exito.
Entonces ahora te cuento como hacerlo sin morir en el intento
PD: hay un glosario al fondo.
Pre-requisitos
Tener un certificado de seguridad de autorizado por afip
Procedimiento General
1. Nos logeamos con el servidor de AFIP mediante el WSAA
2. Obtenemos el TA para poder realizar transacciones con el resto de los servicios
3. Preguntamos el ultimo Codigo usado
4. Mandamos a crear una nueva factura
5. Pedimos el PDF
Primero veremos como conseguir el certificado digital necesario para comunicarnos con los servidores de AFIP. Esto lo veremos en el siguiente post dedicado a eso:
Obtener Certificado de AFIP para Facturacion Electronica
https://exgetmessageaux.blogspot.com.ar/2018/02/obtener-certificado-de-afip-para.html
Ahora veremos como logearnos con el servidor de AFIP para conseguir un TA que nos permita realizar operaciones en los servidores de AFIP.
Los servidores de AFIP para las fechas utilizan el formato yyyyMMdd, es decir para la fecha 21 de enero del 2017, utilizaremos el string "20170121" (2017-01-21)
Estos, utilizan un web service SOAP, que se maneja a travez de XML para la comunicacion, es decir, armamos un XML que explique que queremos hacer, y todos los datos necesarios, y el WS hace lo que puede y nos devuelve otro XML con un resumen de lo que pudo hacer y lo que no.
Con esto veremos ahora en contexto como seria el XML que le vamos a enviar al WSAA para logearnos:
<loginTicketRequest> <header> <uniqueId>1</uniqueId> <generationTime>2017-09-08T08:25:56</generationTime> <expirationTime>2017-09-08T08:45:56</expirationTime> </header> <service>wsfe</service> </loginTicketRequest>
Ahora, el WSAA es el unico que te pide que le envies el XML, el WSFE lo hace a travez de su WSDL, lo que es parecido pero en codigo nativo, sin tocar XML.
Para esto puedo recomendar hacer un template del XML basico y despues modificarlo con cada llamada.
Con motivo de simplificar el tutorial, vamos a hacerlo directamente en un string, que pueda convertir en un objeto XMLDocument y manipularlo con eso.
Si todo sale bien, deberíamos recibir algo de este estilo
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <loginTicketResponse version="1"> <header> <source>CN=wsaahomo, O=AFIP, C=AR, SERIALNUMBER=CUIT 33693450239</source> <destination>SERIALNUMBER=CUIT 20375124685, CN=gestionpersonal</destination> <uniqueId>3594694368</uniqueId> <generationTime>2017-09-08T08:35:53.544-03:00</generationTime> <expirationTime>2017-09-08T20:35:53.544-03:00</expirationTime> </header> <credentials> <token> A very long string </token> <sign> A no so long string </sign> </credentials> </loginTicketResponse>
Donde lo que nos importa es lo que viene en token y en sign, en estos deberían venir dos cadenas de caracteres que nos van a dar el permiso para la sesion.
Bueno, veamos algo de codigo.
Primero creamos un proyecto .NET (en mi caso VB .NET y WinForm) y le agregamos algo para agregar el certificado que vamos a usar, la clave del certificado, el servicio que vamos a usar (todo esto, en nuestro producto final, junto con el CUIT del responsable, deberían estar en una ventana de configuraciones), Un boton de login, y otro de WSFE (para hacer la factura). Podemos agregarle otras cosas como ver lo que estamos enviando y reciviendo (crudo) y un selector de testing y homologacion. La idea de esto es hacer un prototipo de conexion, y que ustedes rescaten lo que les sirve y lo apliquen a su programa.
El proyecto del prototipo esta aca:
GitHub AFIP WSFE WSAA Prototype
La primera ventana nos quedara algo así:
y el código que importa es el que esta atrás del Login:
Private l As LoginClass Private url As String = "https://wsaahomo.afip.gov.ar/ws/services/LoginCms" Private Sub LoginBtn_Click(sender As Object, e As EventArgs) Handles LoginBtn.Click l = New LoginClass(ServicioTX.Text, url, CertificadoTX.Text, ClaveTX.Text) l.hacerLogin() End Sub
Donde LoginClass es la que hace la magia de la conexion. Esta la podemos encontrar aca:
GitHub LoginClass
Pero antes de meternos de lleno en esto, vamos a agregar los WS a travez de sus WSDL (El WSDL describe lo mejor que puede el WS para que directamente Visual Studio u otro, sepa como comunicarse con este)
Para esto seguiremos este post separado
Agregar referencia a WS SOAP AFIP a Visual Studio
https://exgetmessageaux.blogspot.com.ar/2018/02/agregar-referencia-ws-soap-afip-visual.html
Ahora si podemos ver/agregar la clase LoginClass
En el contructor no hay nada en particular, solo toma los datos de Servicio, la URL del servidor que vamos a usar (Homo o Produccion), la ubicacion del Certificado y la clave.Lo importante esta en metodo hacerLogin()
En la primera parte:
'Preparo el XML Request XmlLoginTicketRequest = New XmlDocument XMLLoader.loadTemplate(XmlLoginTicketRequest, "LoginTemplate") uniqueIdNode = XmlLoginTicketRequest.SelectSingleNode("//uniqueId") generationTimeNode = XmlLoginTicketRequest.SelectSingleNode("//generationTime") ExpirationTimeNode = XmlLoginTicketRequest.SelectSingleNode("//expirationTime") ServiceNode = XmlLoginTicketRequest.SelectSingleNode("//service") generationTimeNode.InnerText = DateTime.Now.AddMinutes(-10).ToString("s") ExpirationTimeNode.InnerText = DateTime.Now.AddMinutes(+10).ToString("s") uniqueIdNode.InnerText = CStr(_globalId) ServiceNode.InnerText = serv
Simplemente traemos el template del XML (el que hablamos antes) que vamos a enviar y le seteamos todos los campos (UniqueId, GenerationTime, ExpirationTime y Service)
PD: Para traer el Template use una pequeña clase que use en otros proyectos, que esta en el fondo de la misma clase, la misma se llama XMLLoader.
Acá se vuelve un poco mas complicado:
'Obtenemos el Cert certificado = New X509Certificate2 If clave.IsReadOnly Then certificado.Import(File.ReadAllBytes(cert_path), clave, X509KeyStorageFlags.PersistKeySet) Else certificado.Import(File.ReadAllBytes(cert_path)) End If Dim msgBytes As Byte() = Encoding.UTF8.GetBytes(XmlLoginTicketRequest.OuterXml)
Ahora puede ocurrir un problema, si el sistema no nos deja crear el Certificado, osea que no nos reconoce la clase X509Certificate2 vamos a tener que agregar la referencia a Seguridad de .NET de la siguiente forma:
Vamos a My Project en el Explorador de Soluciones
Ahora, en las pestañas izquierdas vamos a donde dice Referencias
De ahí vamos a abajo donde dice Agregar
Ahora buscamos en el panel lateral, en la sección donde dice Ensablados, y dentro de este buscamos el que dice Framework, cuando lo seleccionemos nos actualizara la lista de librerías, ahora buscaremos en el panel central la librería de System.Security , chequeamos el checkbox que nos aparece en el nombre y le damos a Aceptar.
Y luego nos aseguramos de tener el siguiente import en nuestra clase:
Imports System.Security.Cryptography.X509Certificates
Ahora si, volviendo al LoginClass, la siguiente parte es firmar el mensaje:
'Firmamos Dim infoContenido As New ContentInfo(msgBytes) Dim cmsFirmado As New SignedCms(infoContenido) Dim cmsFirmante As New CmsSigner(certificado) cmsFirmante.IncludeOption = X509IncludeOption.EndCertOnly cmsFirmado.ComputeSignature(cmsFirmante) cmsFirmadoBase64 = Convert.ToBase64String(cmsFirmado.Encode())
Ahora tenemos el XML Firmado y codificado en la variable cmsFirmadoBase64 que es la que vamos a enviar a la AFIP para que nos autorice.
Ya ahora enviamos la info:
'Hago el login Dim servicio As New WSAA.LoginCMSService servicio.Url = url loginTicketResponse = servicio.loginCms(cmsFirmadoBase64)
Si todo salio bien en la variable loginTicketResponse vamos a tener el XML con la respuesta en forma de String, El siguiente paso, es convertirlo a XML Document (Object) y analizarlo, a ver que nos responde el servidor:
Analizamos la respuesta XmlLoginTicketResponse = New XmlDocument XmlLoginTicketResponse.LoadXml(loginTicketResponse) _Token = XmlLoginTicketResponse.SelectSingleNode("//token").InnerText _Sign = XmlLoginTicketResponse.SelectSingleNode("//sign").InnerText Dim exStr = XmlLoginTicketResponse.SelectSingleNode("//expirationTime").InnerText Dim genStr = XmlLoginTicketResponse.SelectSingleNode("//generationTime").InnerText ExpirationTime = DateTime.Parse(exStr) GenerationTime = DateTime.Parse(genStr) MsgBox("Exito")
Ok veamos, como después quiero recuperar las cosas después, la mayoría de las cosas las guarde en variables globales de la clase. El documento entero en XmlLoginTicketResponse, El Token en _Token, el Sign o firma en _Sign, y las fechas de generacion y expiracion en las variables ExpirationTime y GenerationTime. Con esto ya tenemos el TA
Entonces cuando hagamos debug, si salio todo bien, deveriamos ver algo como esto:
Si tenemos eso ya estamos logeados contra del servidor de AFIP.
Ahora lo que nos queda es pasarle el informe al servidor de facturación electrónica.
Importante! Este servidor, no necesita que le pasemos los detalles de cada item de la factura, sino un resumen por cada alícuota, es decir, cuanto es el total con 21% de IVA, cuanto con el 10.5%, etc.
Entonces, igual que antes le tenemos que enviar una especie de XML con los datos, a continuación te muestro el XML de muestra, digo especie, porque con el WSDL te lo traduce para que lo manejes como una especie de objetos.
Este seria el XML que tenemos que enviar, ejecutando el método FECAESolicitar (Solicitar CAE de Fact Electronica):
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ar="http://ar.gov.afip.dif.fev1/"> <soapenv:Header/> <soapenv:Body> <FECAESolicitar> <Auth> <!-- Autenticacion --> <Token>PD94.....</Token> <!-- TA TOKEN --> <Sign>tYft0........</Sign> <!-- TA Sign --> <Cuit>33693450239</Cuit> <!-- Mi CUIT --> </Auth> <FeCAEReq> <FeCabReq> <CantReg>1</CantReg> <!-- Cant de Facturas --> <PtoVta>12</PtoVta> <!-- Punto de Venta --> <CbteTipo>1</CbteTipo> <!-- Factura A --> </FeCabReq> <FeDetReq> <FEDetRequest> <Concepto>1</Concepto> <!-- 1: Productos, 2: Servicios --> <DocTipo>80</DocTipo> <!-- Tipo Doc 80: CUIT --> <DocNro>20111111112</DocNro> <!-- Doc del facturado --> <CbteDesde>1</CbteDesde> <!-- Nro Factura --> <CbteHasta>1</CbteHasta> <!-- Tambien Nro Factura --> <CbteFch>20100903</CbteFch> <!-- Fecha del Comprobante, yyyymmdd--> <ImpTotal>184.05</ImpTotal> <!-- Total con impuestos --> <ImpTotConc>0</ImpTotConc> <!-- Importe neto no gravado --> <ImpNeto>200</ImpNeto> <!-- Importe neto gravado Total sin impuestos --> <ImpOpEx>0</ImpOpEx> <!-- Importe Exento --> <ImpTrib>0</ImpTrib> <!-- Suma de Tributos --> <ImpIVA>26.25</ImpIVA> <!-- Suma de IVAs --> <FchServDesde></FchServDesde> <!-- Solo Servicios --> <FchServHasta></FchServHasta> <!-- Solo Servicios --> <FchVtoPago></FchVtoPago> <MonId>PES</MonId> <!-- Codigo de Moenda --> <MonCotiz>1</MonCotiz> <!-- Cotizacion, para pesos: 1--> <!-- <Tributos> Por si retiene tributos <Tributo> <Id>99</Id> <Desc>Impuesto Municipal Matanza</Desc> <BaseImp>150</BaseImp> <Alic>5.2</Alic> <Importe>7.8</Importe> </Tributo> </Tributos> --> <Iva> <AlicIva> <Id>5</Id> <!-- Id segun tabla de IVAs (5: 21%)--> <BaseImp>100</BaseImp> <!-- Importe total para esa alicuota --> <Importe>21</Importe> <!-- Monto total del IVA (100 * 0.21) --> </AlicIva> <AlicIva> <Id>4</Id> <!-- 4: 10.5 --> <BaseImp>50</BaseImp> <Importe>5.25</Importe> </AlicIva> </Iva> </FEDetRequest> </FeDetReq> </FeCAEReq> </FECAESolicitar> </soapenv:Body> </soapenv:Envelope>
Como ven en ningún lado nos piden que items tiene la factura. Entonces eso es responsabilidad de ustedes, del lado de su base de datos de guardar la factura. Incluso, se pueden hacer varias facturas, por ejemplo diciendo que desde la factura 5 a la 8 hay un total de $1210 con $210 de IVA 21%, sin especificarle la cantidad de intems ni cuanto es el total por cada comprobante, eso si, después lo tenemos que separar bien en cada factura que vayamos a entregar.
Como tambien se ve, hay muchos campos que requieren un Id de un elemento. Por ejemplo, En Tipo de Documento, necesita el Id del CUIT (80), o la moneda, que esta el Id de la moneda Peso (PES), u otros. Estos son medianamente fijos, el tema es que pueden cambiarlo sin avisar, o pueden agregar nuevos, Para obtener estos campos se puede hacer a travez del mismo WS.
Primero hacemos una ventana como la del programa, recuerdo que esta es de prueba. para que vean que info se necesita para enviar a la AFIP, ustedes tendrán que incorporarlo a su sistema.
Entonces aca tengo un par de CombosBox, estos los rellenaremos con lo que nos da AFIP.
Rellenamos los Cmbs través del botón de Cargar, así que veamos bien que hace:
Private Sub CargaBtn_Click(sender As Object, e As EventArgs) Handles CargaBtn.Click Try authRequest = New FEAuthRequest() authRequest.Cuit = MyCuitTX.Text authRequest.Sign = Login.Sign authRequest.Token = Login.Token Dim service As WSFEHOMO.Service service.Url = url service.ClientCertificates.Add(Login.certificado) puntosventa = service.FEParamGetPtosVenta(authRequest) ptos_venta_cm.DataSource = puntosventa.ResultGet TiposComprobantes = service.FEParamGetTiposCbte(authRequest) TiposComprobantesCMB.DataSource = TiposComprobantes.ResultGet TipoConceptos = service.FEParamGetTiposConcepto(authRequest) TipoConcepto.DataSource = TipoConceptos.ResultGet TipoDoc = service.FEParamGetTiposDoc(authRequest) TipoDocCMB.DataSource = TipoDoc.ResultGet Monedas = service.FEParamGetTiposMonedas(authRequest) MonedaCMB.DataSource = Monedas.ResultGet TiposIVA = service.FEParamGetTiposIva(authRequest) TipoIVACmb.DataSource = TiposIVA.ResultGet Dim lastCbteObj = service.FECompUltimoAutorizado(authRequest, 4, TiposComprobantes.ResultGet(0).Id) NroCbteTX.Text = lastCbteObj.CbteNro + 1 opcionales = service.FEParamGetTiposOpcional(authRequest) Catch ex As Exception MsgBox(ex.Message) End Try End Sub
Entonces vemos que en las primeras lineas completamos el campo Auth del XML, donde le ingresamos el Cuit propio y el Login y Sign del TA que conseguimos en la Autenticación.
En la linea service.ClientCertificates.Add(Login.certificado) Nos encargamos de agregar el certificado que nos fue asignado para trabajar con esto, el mismo que usamos antes. Si se dan cuenta van a ver que me traje el objeto Login del formulario anterior con todo lo necesario.
Luego por cada item que llenemos vamos a tener un par de lineas
TiposComprobantes = service.FEParamGetTiposCbte(authRequest) TiposComprobantesCMB.DataSource = TiposComprobantes.ResultGet
Y la segunda es la que asigna el set de resultado al ComboBox. Asegúrense de tener seteado el atributo DisplayMember de cada Combobox, sino mostrara cualquier cosa.
Esto es para las opciones parametrizadas, como el tipo de comprobante, tipo de moneda, tipo de iva, etc
Y con eso se nos debería llenar todos los CMB, quedándonos algo así.
Para este ejemplo usaremos un solo IVA (suponiendo que todos los items dentro de la factura tienen la misma alícuota. Entonces para este caso, y como la mayoría los precios ya incluyen el IVA, les puse las dos posibilidades, la de ingresar el Total Neto (Sin Iva) y la Alícuota y que te calcule el total, o al revés, Ingresando el Total y la alícuota, te calcule el Total Neto.
No voy a ponerme a explicar todo el código, es bastante simple, y si tienen alguna duda pueden consultarme por mail o en la caja de comentarios.
Ahora paso a explicar como hacer la conexión a la AFIP para pesarla el resumen de la compra, haciendo esto, ellos nos devuelven el Código de Autorización Electrónico (CAE) y su Vencimiento, elementos necesario para que la factura que imprimamos sea valida.
Para eso veremos el código llamado al hacer clic en el botón "Registrar"
Primero en esta parte
Dim service As WSFEHOMO.Service = getServicio() service.ClientCertificates.Add(Login.certificado) Dim punto_seleccionado As PtoVenta = ptos_venta_cm.SelectedItem Dim cm As CbteTipo = TiposComprobantesCMB.SelectedItem Dim req As New FECAERequest Dim cab As New FECAECabRequest Dim det As New FECAEDetRequest cab.CantReg = 1 cab.PtoVta = punto_seleccionado.Nro cab.CbteTipo = cm.Id req.FeCabReq = cab
Luego obtenemos el punto de venta seleccionado al igual que el tipo de comprobante.
Luego empezamos a armar el Request. Primero inicializamos El Request (FECAERequest), la cabecera (FECAECabRequest) y el cuerpo (FECAEDetRequest) vacíos.
Como ya dije, en este procedimiento podemos enviar mas de una factura a la vez, por ahora enviaremos de a una a la vez. Entonces seteamos el campo Cantidad de Registros (CantReg) de la cabecera en 1. Seteamos el campo Pto de Venta y Tipo de Comprobante (PtoVta, CbteTipo) con los parámetros seleccionados. Y no nos olvidemos de setear la cabecera del Request (FeCabReq).
Dim concepto As ConceptoTipo = TipoConcepto.SelectedItem det.Concepto = concepto.Id Dim doctipo As DocTipo = TipoDocCMB.SelectedItem det.DocTipo = doctipo.Id det.DocNro = Long.Parse(DocTX.Text)
Dim este_cbte = ultimo_nro + 1 det.CbteDesde = este_cbte det.CbteHasta = este_cbte det.CbteFch = FechaDTP.Value.ToString("yyyyMMdd") det.ImpNeto = NetoTX.Text det.ImpIVA = ImpIvaTx.Text det.ImpTotal = TotalTx.Text det.ImpTotConc = 0 det.ImpOpEx = 0 det.ImpTrib = 0
Ponemos en cero los totales no gravados (ImpTotConc), Excentos (ImpOpEx) y Tributos (ImpTrib) ya que solo incluiremos Iva 21% sin recaudar tributos.
Dim mon As Moneda = MonedaCMB.SelectedItem det.MonId = mon.Id det.MonCotiz = 1 Dim alicuota As New AlicIva Dim ivat As IvaTipo = TipoIVACmb.SelectedItem alicuota.Id = ivat.Id alicuota.BaseImp = NetoTX.Text alicuota.Importe = ImpIvaTx.Text det.Iva = {alicuota} req.FeDetReq = {det}
Ahora creamos un objeto AlicoutaIva y le setamos los parámetros de IVA según lo seleccionado, la base imponible (a cuanto se le aplica el IVA) y el Importe de IVA.
Seteamos el parámetro IVA del detalle (el cual es un array), si tuviéramos mas de una alícuota de IVA el array IVA tendría que tener mas de un elemento como en el ejemplo.
Por ultimo seteamos el Detalle del Request (El cual tambien es un array) con el objeto que estuvimos configurando. Si tuviéramos mas de un detalle, el array de detalle tendría mas de un objeto.
Con esto ya queda configurado el Request, listo para enviar a la AFIP.
Dim respuesta As FECAEResponse = service.FECAESolicitar(authRequest, req) Dim m As String = "Estado: " & respuesta.FeCabResp.Resultado & vbCrLf m &= "Estado Esp: " & respuesta.FeDetResp(0).Resultado m &= vbCrLf m &= "CAE: " & respuesta.FeDetResp(0).CAE m &= vbCrLf m &= "Vto: " & respuesta.FeDetResp(0).CAEFchVto m &= vbCrLf m &= "Desde-Hasta: " & respuesta.FeDetResp(0).CbteDesde & "-" & respuesta.FeDetResp(0).CbteHasta m &= vbCrLf If respuesta.FeDetResp(0).Observaciones IsNot Nothing Then For Each o In respuesta.FeDetResp(0).Observaciones m &= String.Format("Obs: {0} - {1}", o.Code, o.Msg) & vbCrLf Next End If m &= vbCrLf If respuesta.Errors IsNot Nothing Then For Each er As Err In respuesta.Errors m &= String.Format("Err: {0} - {1}", er.Code, er.Msg) Next End If m &= vbCrLf If respuesta.Events IsNot Nothing Then For Each ev As Evt In respuesta.Events m &= String.Format("Evt: {0} - {1}", ev.Code, ev.Msg) Next End If Resultado.Text = m
Seguido, armamos un resumen re la respuesta para mostrar.
Igual que antes, la respuesta va a tener tantos detalles como detalles tenga la solicitud.
Como solo tenemos un detalle en la solicitud, esperamos un solo detalle en la respuesta.
Cualquier observación particular se va a encontrar en el array Observaciones de cada detalle.
Las observaciones generales sobre la solicitud se encontraran en el Array Events de la respuesta (respuesta.Events) y los Errores del proceso en la variable Errors de la respuesta (respuesta.Errors).
Si todo salio bien. El CAE se va a encontrar en el detalle correspondiente, junto con su vencimiento.
Podemos verificar rápidamente el resultado del proceso a través del atributo Resultado de la cabecera (resultado general del proceso) o de cada detalle (Particular de ese detalle), en donde R significa Rechazado y A Aprobado.
Ya con eso queda registrada la factura con AFIP. Queda como responsabilidad nuestra entonces guardar en nuestra base de datos el detalle de cada factura emitida.
Cada factura que enviemos a la AFIP y este aprueba no solo queda guardado en sus servidores, sino que queda a nuestra entera disposición para consultar si así lo necesitáramos.
Así podemos hacer un sistema de comprobación de integridad, comparando lo que tenemos en nuestra base de datos contra lo que tienen ellos, según lo que nosotros enviamos.
Una vez que completan la parte de homologacion simplemente tienen que cambiar el link al Service de Produccion o tenerlos los dos y usarlo según se quiera.
Hasta acá voy a explicar. Son libres de hurgar por el código del prototipo.
Espero les sirva. Y cualquier duda esta la caja de comentarios.
Bibliografia
https://drive.google.com/drive/folders/0B5Xpb7ydUT4XLTU0VjBFQ3FVTEE?resourcekey=0-NvZkDioiFyg_B_HN0mZ4bQ&usp=sharing
Glosario
AFIP - Administración Federal de Ingresos Públicos
CAE - Codigo de Autorizacion Electronico
CAEA - CAE Anticipado
CSR - Certificate Singin Request, Requisito de Certificado de Firma
FE - Facturacion Electronica
Sign - Firma que autoriza el Token
TA - Ticket de Autenticación
WS - Web Service
WSAA - Web Service de Autenticación y Autorización
WSDL - Web Service Description Lenguage (Lenguaje de descripción de WS)
WSFE - Web Service de Facturacion Electronica
WSN - Web Services de Negocios