Ver Mensaje Individual
  #1  
Antiguo 28-10-2015
espinete espinete is offline
Miembro
 
Registrado: mar 2009
Posts: 233
Reputación: 16
espinete Va camino a la fama
Facturas electrónicas [España]

NOTA DEL MODERADOR:=======================================
Como hemos hecho con otros temas similares, voy a "fijar" este hilo para dedcarlo a la Facturación electrónica (Facturae).
Además adjunto links de otros hilos (que no quedan fijados) sobre el mismo tema:

=======================================================

[Movido desde esta otra conversación]

Bueno, voy a aportar lo que tengo por ahora, que es la parte que crea el .XML (o .XSIG para la Admón. Pública) y lo firma con certificado digital o DNIe. Pero esto no puede quedarse ahí, hay que seguir con la tercera parte (que es la finalidad de este post), que es enviar la factura al webservice de face, a ver si entre todos lo conseguimos.

Con este código se puede crear un .XML (manualmente) y firmarlo para que sea válido y aceptado en la web de facturae. La web de facturae tiene un "validador online" aquí...
http://sedeaplicaciones2.minetur.gob.es/FacturaE/
...que nos servirá para comprobar si las facturas creadas son correctas, están bien firmadas, etc.

Componentes necesarios de SecureBlackBox: TEIWinCertStorage y TEIX509Certificate.
Otros componentes: TXMLDocument

(los componentes SecureBlackBox vienen con un montón de proyectos de ejemplo, entre ellos uno para firmar archivos PDF o XML).

Primera parte: crear el .XML

Yo he decidido crearlo "a mano". Un XML no es más que un archivo de texto con una estructura concreta, así que utilizo un Memo y le voy añadiendo las líneas con cuidado...
No voy a ponerlo todo porque es muy largo. Quizás más tarde subiré el código completo. El XML requiere un montón de valores variables que debemos rellenar (datos del emisor, receptor, factura, artículos, totales, impuestos...). Esta parte es algo tediosa, pero muy sencilla.

Antes de nada, cargamos los certificados digitales instalados en el sistema, por ejemplo en el OnCreate del Form1, en un ComboBox.
Esto hará que en el combobox veamos los certificados digitales instalados en Windows, ya sean certificados de empresa, el del DNIe, etc.
Nos harán falta más tarde para la firma del XML.

Código Delphi [-]
procedure TForm1.FormCreate(Sender: TObject);
var i:integer;
begin
    for i := 0 to WinCertStorage.Count - 1 do
    begin
      Cert := WinCertStorage.Certificates[i];
      ComboCertificate.Items.Add('Título: ' + Cert.SubjectName.CommonName + ', Emisor: ' + Cert.IssuerName.CommonName);
    end;
end;

Empezamos a crear el XML a mano...

Código Delphi [-]
      memo1.Lines.Clear;
      memo1.lines.append('');
      memo1.lines.append(' xmlns:fe="http://www.facturae.es/Facturae/2009/v3.2/Facturae" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"');
      memo1.lines.append('');
      memo1.lines.append('3.2');
      {.....}

Cuando tengamos todo el texto del XML, guardamos el memo en un archivo con extensión xml.

Código Delphi [-]
    memo1.Lines.SaveToFile('c:\pruebas\factura.xml');

Ahora abrimos el archivo con el componente XMLDocument para tenerlo ahí cargado, y lo volvemos a guardar. ¿por qué? Porque así se guarda bien estructurado, identado, etc. y es más fácil leerlo si nos hiciera falta.

Código Delphi [-]
    XMLDocument1.LoadFromFile(archivo);
    xmldocument1.Active:=true;
    xmldocument1.SaveToFile(archivo);

Y ahora a firmar...

Segunda parte: Firmar el XML

Este código no es mío. Tiene partes extraídas de clubdelphi, de los ejemplos de secureblackbox y de otros ejemplos de firma digital.
En el código le decimos qué certificado utilizar para la firma, de entre los cargados en el combobox anterior.
Probablemente el código se pueda mejorar. He dejado los comentarios del creador original porque explican bastante bien lo que se hace en él.

Código Delphi [-]
function Firma:boolean;
var XML_Doc:ElXMLDOMDocument;
    XML_Refs:TElXMLReferenceList;
    XML_RefDocu,XML_RefCert:TElXMLReference;
    XML_Signer:TElXMLSigner;
    XML_XAdES:TElXAdESSigner;
    XML_KeyData:TElXMLKeyInfoX509Data;
    XML_Nodo:ElXMLDOMNode;
    MS:TStream;
    XML_Buf:ByteArray;

    F: {$ifndef DELPHI_NET}TFileStream{$else}FileStream{$endif};
begin
    result:=false;
    with form1 do
    begin

          // ************************
          // ** Firmar fichero XML **
          // ************************

          XML_Doc:=     ElXMLDOMDocument.create;         //Documento XML a ser firmado
          XML_Refs:=    TElXMLReferenceList.create;          //Lista de nodos a ser firmados
          XML_RefDocu:= TElXMLReference.create;            //Nodo que representa al XML completo
          XML_RefCert:= TElXMLReference.create;             //Nodo del certificado utilizado
          XML_Signer:=  TElXMLSigner.Create(nil);            //Objeto firmante en XML...
          XML_XAdES:=   TElXAdESSigner.Create(nil);       //Sujeto firmante con info adicional XAdES
          XML_KeyData:= TElXMLKeyInfoX509Data.create(false); //Certificado a utilizar...

          try
            //Leo el fichero XML
            //==================
            // Por defecto en UTF-8 y normalizando los finales de linea (si llegan
            // CR+LF entonces la firma no se validará luego pues los digest no
            // deberían incluir estos CR+LF).

            {$ifndef DELPHI_NET}
            F := TFileStream.Create(archivo, fmOpenRead or fmShareDenyWrite);
            {$else}
            F := FileStream.Create(archivo, FileMode.Open, FileAccess.Read);
            {$endif}
            try
              XML_Doc.LoadFromStream(F, '', true);
            except
              on E : Exception do
              begin
                MessageDlg('Error: ' + E.Message, mtError, [mbOk], 0);
              end;
            end;

            FreeAndNil(F);

            if not XML_Doc.Loaded then
              raise Exception.create('Firma XML: No se pudo cargar el documento XML.');

            //Configuro el objeto firmador de XML
            //===================================
            // Se han de firmar la lista de nodos referenciados en XML_Refs
            XML_Signer.References:= XML_Refs;
            XML_Signer.SignatureMethodType:= xmtSig;
            // Se firma en formato "enveloped"
            XML_Signer.SignatureType:= xstEnveloped;
            // Se canoniza -ver ejemplos de la web facturae- el XML
            XML_Signer.CanonicalizationMethod:= xcmCanon;
            // La firma con el metodo mas estandar posible
            XML_Signer.SignatureMethod:= xsmRSA_SHA1;
            // La parte publica del certificado se debe incluir
            XML_Signer.IncludeKey:= true;
            // Necesito incrustar info adicional "XAdES" (XML Advanced Electronic Signatures )...
            XML_XAdES:= TElXAdESSigner.Create(nil);
            XML_Signer.XAdESProcessor:= XML_XAdES;

            //Configuro la parte XAdES del objeto firmador
            //============================================
            //
            // Se pide usar XAdES V1.2.2 o superior pero ha de ser "compatible" (?)
            // Algunos ejemplos usando V1.3.2 validan, asi que lo uso.
            XML_Signer.XAdESProcessor.XAdESVersion:= XAdES_v1_3_2;
            //SignerRole (en calidad de qué firmamos): supplier, customer o third_party
            //  Firmamos facturas emitidas por nosotros (supplier) en este caso.
            //  Si recibimos una factura firmada por el supplier, la podemos volver
            //  a firmar como que la damos por recibida usando "customer".
            //NOTA: El validador de www.facturae.es no comprueba este dato (2-2010)
            XML_Signer.XAdESProcessor.Included := [xipSignerRole];
            XML_Signer.XAdESProcessor.SignerRole.ClaimedRoles.AddText(
              XML_Signer.XAdESProcessor.XAdESVersion, XML_Doc, 'supplier');
            // La politica de firmado es la definida en el PDF V3.1 sobre firmado Facturae
            XML_Signer.XAdESProcessor.PolicyId.SigPolicyId.Identifier:= 'http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf';
            //NOTA: Si vas a la web, este PDF esta en otro link diferente (WTF):
            //'http://www.facturae.es/es-ES/Documentacion/Politicas/Politicas/Versi%C3%B3n%203_1/Politica_Firma_formato_facturae_v3_1.pdf';
            XML_Signer.XAdESProcessor.PolicyId.SigPolicyId.Description:= 'Política de firma electrónica para facturación electrónica con formato Facturae';
            //Ahora el hash del propio PDF en SHA1 (en la web esta debajo del PDF)...
            XML_Signer.XAdESProcessor.PolicyId.SigPolicyHash.DigestMethod:= 'http://www.w3.org/2000/09/xmldsig#sha1';
            //El digest en SHA-1 de la web (en formato hex), esta MAL, no corresponde
            //con ese PDF, y ademas, en su ejemplo de la V3.0 el digest es correcto
            //y NO se corresponde con el digest que dan!
            // '613c46e7bac7df5b266e6be0349b5fe8bb4944e2' ESTA MAL EN LA WEB
            //Uso el digest SHA1 correcto generado a partir del PDF:
            SetLength(XML_Buf,0); //Evito un warning tonto
            if not HexStr2ByteArray('3A18B197ABA90FA6AFF0DEE912F0C006110BEA13', XML_Buf) then
              raise Exception.Create('Firma XML: Error convirtiendo digest de política de firmado a Base64.');
            {NOTA: Sustituido por  HexStr2ByteArray()
            XML_Digest:= LowerCase('3A18B197ABA90FA6AFF0DEE912F0C006110BEA13');
            SetLength(XML_Buf, Length(XML_Digest) div 2);
            //En delphi 7 HexToBin va bien, en Delphis mas moderno, no vale porque
            //WideChar y pChar se tratan de forma diferente, asi que se hace a mano.
              //HexToBin(PChar(XML_Digest), PChar(XML_Buf), Length(XML_Digest) div 2);
            for j:= 0 to Length(XML_Buf)-1 do begin
              k1:= SBMath.HexToDecDigit(XML_Digest[j*2 + 1]);
              k2:= SBMath.HexToDecDigit(XML_Digest[j*2 + 2]);
              if (k1<0) or (k2<0) then
                raise Exception.Create('Firma XML: Error convirtiendo digest de política de firmado a Base64.');
              XML_Buf[j]:= k1 shl 4 + k2;
            end;}

            XML_Signer.XAdESProcessor.PolicyId.SigPolicyHash.DigestValue:= XML_Buf;

            //-----------------------
            // FECHA/HORA DE LA FIRMA
            //-----------------------
            //Anoto la fecha y hora del firmado (del PC no es legalmente valida)

            XML_Signer.XAdESProcessor.SigningTime := LocalTimeToUTCTime(Now);

            //Pre-Genero los nodos de firmado necesarios
            //==========================================
            // Esta es la parte mas "confusa": He de firmar partes del XML que no
            // existen hasta que este firmado ¿Como? El objeto Signer, junto con el
            // prepocesador XAdES, crean esos nuevos nodos para que podamos
            // referenciarlos -decirle al firmador que son unas referecnias a ser
            // firmadas- y al final, al firmar, unira el XML original con estos
            // nuevos nodos -formato de firma "enveloped"- y se graba el XML final.
            //
            // Primero genero la zona de informacion XAdES:
            XML_Signer.XAdESProcessor.Generate;
            // Ahora, al objeto firmador "generico" le pido lo mismo, con lo que
            // tambien se crea el nodo "KeyInfo" con el certificado a utilizar.
            XML_Signer.UpdateReferencesDigest;
            //Cargo certificado para firmar
            //=============================
            // Cargo el certificado a usarse en la firma en el objeto firmador
            // NOTA: Se incluira la parte publica del certificado en base64.
            // De los certificados que me pases, elijo el primero con parte privada,
            // ya que no quiero incluir el certificado raiz del emisor (no se
            // menciona en la politica de firmado y no aparece en los ejemplos):

            XML_KeyData.IncludeKeyValue:= true;
           Cert := WinCertStorage.Certificates[comboCertificate.ItemIndex];
           if Cert.PrivateKeyExists then
               XML_KeyData.certificate:= Cert;

            //Compruebo que ha quedado algún certificado válido...
            if not Assigned(XML_KeyData.certificate) then
              raise Exception.create('FirmaXML: No se cargó un certificado válido para firmar, no contiene clave privada.');
            XML_Signer.KeyData:= XML_KeyData;
            //Añado a la lista de nodos a firmar
            //==================================
            //NOTA: El metodo SHA1 no es el mas seguro, pero el SHA256 no se usa mucho
            //y puede dar problemas (WinXP SP2 no lo admite) por eso el DNIe tampoco
            //lo usa aun, asi que mejor dejo el valor por defecto.
            //
            // NODO 1: Se ha de firmar el documento original XML completo...
            //
            XML_RefDocu.DigestMethod:= xdmSHA1; //El mas estandard/compatible de todos
            XML_RefDocu.URINode:= XML_Doc.DocumentElement; //El XML completo
            XML_RefDocu.URI:= ''; //No hay nombre especifico para este nodo
            // En los ejemplos aparece la transformacion "Enveloped-Signature"
            // aunque en el PDF de la firma V3.1 no lo menciona.
            XML_RefDocu.TransformChain.Add(TElXMLEnvelopedSignatureTransform.Create);
            XML_Refs.Add(XML_RefDocu); //Lo sumo a las cosas a ser firmadas
            // Actualizo digest con este nuevo nodo.
            XML_Signer.UpdateReferencesDigest;
            // NODO 2: Se han de firmar las propiedades de firmado (XAdES)....
            //
            // El nodo SignedProperties es parte de la informacion XAdES que se
            // creo en XML_Signer.XAdESProcessor.Generate y se firma siempre en
            // el standard XAdES. Dejo el codigo abajo como referencia solo, pero
            // no funcionaría si se activa porque ya esta siendo firmado una vez.
            //XML_Signer.XAdESProcessor.QualifyingProperties.SignedProperties.ID:= 'SignedPropertiesID';
            //XML_RefProp.DigestMethod:= xdmSHA1;
            //XML_RefProp.URI:= '#SignedPropertiesID';
            //XML_Refs.Add(XML_RefProp);
            //XML_Signer.UpdateReferencesDigest;
            //
            // NODO 3: Se ha de firmar el propio certificado utilizado
            //
            // El nodo KeyInfo ha de ir identificado por 'Certificate1' -segun los
            // ejemplos, el PDF no da ningun nombre concreto-
            // si se firma dos veces un mismo XML no chocan aunque el
            // nombre coincida por estar dentro de diferentes nodos "Signature".

            XML_RefCert.URI:= '#Certificate1';
            XML_RefCert.DigestMethod:= xdmSHA1;
            XML_Refs.Add(XML_RefCert); //Lo sumo a las cosas a ser firmadas
            //Como ya tengo todos los nodos, ahora genero la firma y consigo que
            //exista el nodo KeyInfo, de forma que pueda darle el nombre que le toca
            //antes de unir el XML original con el nuevo nodo de la firma.
            XML_Signer.Sign;
            XML_Signer.Signature.KeyInfo.ID:= 'Certificate1';
            //Añado la firma al XML
            //=====================
            XML_Nodo:= ElXMLDOMNode(XML_Doc.DocumentElement);
            XML_Signer.Save(XML_Nodo);
            //Actualizo el stream MS con el XML final firmado.

            {$ifndef DELPHI_NET}
            F := TFileStream.Create(archivo, fmCreate or fmOpenWrite);
            {$else}
            F := System.IO.FileStream.Create(archivo, FileMode.Create, FileAccess.ReadWrite);
            {$endif}
            try
              XML_Doc.SaveToStream(F, xcmNone, '');
            except
              on E : Exception do
              begin
                MessageDlg('Error: ' + E.Message, mtError, [mbOk], 0);
              end;
            end;
            FreeAndNil(F);

            result:= true;
          finally
            //NOTA: Los XML_Ref se destruyen solos junto al XML_Refs
            XML_Doc.free;
            XML_Refs.free;
            XML_Signer.free;
            XML_XAdES.free;
            XML_KeyData.Free;
          end;
    end;
end;

Y ya tenemos firmado nuestro XML. Y si el certificado es válido, la fecha/hora es válida, etc. entonces la factura electrónica es perfectamente legal. Podremos comprobarlo en la web de validación de facturae, que además nos dirá donde está el error, si lo hubiera.

IMPORTANTE:
Cuando la factura es para una Administración Pública, el .XML debe tener unas secciones obligatorias (Oficina contable, órgano gestor, etc.) y el archivo, en vez de tener extensión XML, lo guardaremos al final como .xsig. Y eso es todo.

Y ahora, lo difícil...

Enviar la factura al webservice...

Importamos el WSDL del webservice del face desde Delphi. Hay dos versiones, la de producción y la de desarrollo para pruebas. El de pruebas es este: https://se-face-webservice.redsara.es/sspp?wsdl

En el IDE, vamos a Component -> Import WSDL e indicamos la url anterior. Siguiente -> Siguiente y Finish.
Esto creará una unit llamada 'sspp.pas', que guardaremos en la misma carpeta de nuestro proyecto, y añadiremos al uses de nuestro form.

Y hasta aquí puedo leer. Y no porque no quiera seguir, sino porque no tengo ni idea.

Este es el código, supuestamente, para enviar la factura al webservice, pero antes hay que hacerle varias cosas que desconozco. Creo que incluso puede hacerse sin los SecureBlackBox, pero de verdad que estoy atascado.

Código Delphi [-]
var facturasspp : SSPPFactura;
    fichero_fac : SSPPFicheroFactura;
    answ : SSPPResultadoEnviarFactura;
    PO:SSPPWebServiceProxyPort;
begin
          PO := GetSSPPWebServiceProxyPort(FALSE, '', nil);
          facturasspp := ssppfactura.Create;
          facturasspp.correo := 'correo@micorreo.com';
          fichero_fac := ssppficherofactura.Create;
          fichero_fac.nombre := 'c:\pruebas\factura.xml';
          fichero_fac.factura := '12345';
          fichero_fac.mime := 'application/xml';
          facturasspp.fichero_factura := fichero_fac;

          try
            answ:=PO.enviarFactura(facturasspp);
          except
            on e:exception do showmessage(e.Message);
          end;
end;

Última edición por Neftali [Germán.Estévez] fecha: 04-10-2022 a las 11:22:50. Razón: Movido desde otro hilo.
Responder Con Cita