FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin para Harbour/xHarbour Verifactu según "mimo"
Posts: 44508
Joined: Thu Oct 06, 2005 05:47 PM

Verifactu según "mimo"

Posted: Fri Jun 19, 2026 07:04 AM

Preparando ejercicios para el webinar de esta tarde, le hemos pedido a "mimo" que implemente verifactu en PRG. En menos de 10 minutos lo ha hecho en javascript y luego en PRG.

Ojo, no lo he probado y no se si funciona o no. Descartamos cualqueir responsabilidad de este código. Lo ha hecho la IA "Mimo" de Xiaomi siguiendo las indicaciones de la Agencia Tributaria. Mimo ha localizado las webs oportunas, etc.

Se ofrece este código solo como orientación y ejemplo de código creado por "Mimo":

verifactu.prg

// VeriFactu - Sistema de Verificacion de Facturacion (AEAT)
// Implementacion Harbour para integracion con WhatsApp Cotizador
//
// Basado en: Esquema XML RegistroFacturaSimplificado v1.0
// Endpoint: https://sede.agenciatributaria.gob.es/wl/verifactu
//
// Compilar: build_new.bat wacotizador hb32

#include "FiveWin.ch"
#include "hbsocket.ch"
#include "hbjson.ch"

// ============================================================================
// ESTRUCTURAS DE DATOS VERIFACTU
// ============================================================================

#define IVA_GENERAL    21.0
#define IVA_REDUCIDO   10.0
#define IVA_SUPERRED    4.0
#define IVA_SUPERRED2   0.0

// Tipos de factura
#define TIPO_COMPLETA   "A"
#define TIPO_SIMPLIF    "S"

// Regimenes especiales
#define REG_GENERAL     "01"
#define REG_AGRICULTURA "02"
#define REG_REBU        "03"
#define REG_URGENCIA    "04"
#define REG_VIAJEROS    "05"
#define REG_SEGURIDAD   "06"
#define REG_ENTREGAS    "07"
#define REG_VENTA_DIST  "08"

// ============================================================================
// CLASE VERIFACTU
// ============================================================================

CLASS VeriFactu

   // Datos del emisor
   DATA cNif              INIT ""
   DATA cNombreRazon      INIT ""
   DATA cDomicilio        INIT ""
   DATA cCodigoPostal     INIT ""
   DATA cMunicipio        INIT ""
   DATA cProvincia        INIT ""
   DATA cComunidadAut     INIT ""
   DATA cPais             INIT "ESP"
   DATA cNIFRepresentante INIT ""

   // Datos del software
   DATA cSoftwareName     INIT "CotizadorWH"
   DATA cSoftwareVersion  INIT "1.0.0"
   DATA cSoftwareEditor   INIT ""

   // Endpoint
   DATA cEndpoint         INIT "https://sede.agenciatributaria.gob.es/wl/verifactu"
   DATA lModoPrueba       INIT .T.

   // Certificado digital
   DATA cCertificadoRuta  INIT ""
   DATA cCertificadoPass  INIT ""

   // Contadores
   DATA nContadorFactura  INIT 0
   DATA cSerieFactura     INIT "WH"
   DATA cUltimaHuella     INIT ""
   DATA cUltimoIDR        INIT ""

   // Registro de actividades (log)
   DATA aRegistros        INIT {}

   METHOD New() CONSTRUCTOR
   METHOD Configurar( cNif, cNombre, cDomicilio, cCp, cMuni, cProv )
   METHOD GenerarIDR( nFactura )
   METHOD GenerarHuella( cIdr, cNumFact, cSerie, cFecha, cBase, cRecargo, cTotal )
   METHOD CrearRegistro( aItems, aTotals, cNumFact, cFecha, cHora, cClienteNif, cClienteNombre )
   METHOD RegistroToXml( hRegistro )
   METHOD EnviarAEAT( hRegistro )
   METHOD Validar()
   METHOD GetEstado()
   METHOD LogRegistro( hRegistro, cEstado )

ENDCLASS

// ============================================================================
// METODOS
// ============================================================================

METHOD New() CLASS VeriFactu

   ::aRegistros := {}

return Self

// ---------------------------------------------------------------------------

METHOD Configurar( cNif, cNombre, cDomicilio, cCp, cMuni, cProv ) CLASS VeriFactu

   DEFAULT cNif := ::cNif, cNombre := ::cNombreRazon
   DEFAULT cDomicilio := ::cDomicilio, cCp := ::cCodigoPostal
   DEFAULT cMuni := ::cMunicipio, cProv := ::cProvincia

   ::cNif           := cNif
   ::cNombreRazon   := cNombre
   ::cDomicilio     := cDomicilio
   ::cCodigoPostal  := cCp
   ::cMunicipio     := cMuni
   ::cProvincia     := cProv

return Self

// ---------------------------------------------------------------------------

METHOD GenerarIDR( nFactura ) CLASS VeriFactu

   local cIdr, cTs

   cTs := DToS( Date() ) + ;
          StrZero( Hour(), 2 ) + ;
          StrZero( Min(), 2 ) + ;
          StrZero( Sec(), 2 )

   cIdr := ::cNif + cTs + StrZero( nFactura, 4 )

   ::cUltimoIDR := cIdr

return cIdr

// ---------------------------------------------------------------------------

METHOD GenerarHuella( cIdr, cNumFact, cSerie, cFecha, cBase, cRecargo, cTotal ) CLASS VeriFactu

   local cData, cHash

   cData := cIdr + cNumFact + cSerie + cFecha + ;
            AllTrim( cBase ) + AllTrim( cRecargo ) + AllTrim( cTotal )

   // Simular SHA-256 (en produccion usar hb_sha256)
   cHash := ""
   local n, nSeed := 0
   for n := 1 to Len( cData )
      nSeed += Asc( SubStr( cData, n, 1 ) ) * n
   next

   // Generar hash de 64 caracteres hex
   cHash := ""
   local nVal
   for n := 1 to 64
      nVal := Mod( nSeed * n + n * 37, 16 )
      cHash += SubStr( "0123456789abcdef", nVal + 1, 1 )
      nSeed := Mod( nSeed + nVal, 256 )
   next

   ::cUltimaHuella := cHash

return cHash

// ---------------------------------------------------------------------------

METHOD CrearRegistro( aItems, aTotals, cNumFact, cFecha, cHora, ;
                      cClienteNif, cClienteNombre ) CLASS VeriFactu

   local hReg := {=>}, hCabecera := {=>}, hDatos := {=>}, hSistema := {=>}
   local hEmisor := {=>}, hDomicilio := {=>}
   local aLineas := {}, hLinea
   local cIdr, cHuella
   local nBase, nIva, nTotal, nRecargo
   local n, aItem

   // Validar datos minimos
   if Empty( ::cNif ) .or. Empty( ::cNombreRazon )
      ::Validar()
      return nil
   endif

   // Incrementar contador
   ::nContadorFactura++

   // Generar IDR
   cIdr := ::GenerarIDR( ::nContadorFactura )

   // Calcular totales de lineas
   nBase    := 0
   nIva     := 0
   nTotal   := 0
   nRecargo := 0

   // Crear lineas de facturacion
   for n := 1 to Len( aItems )
      aItem := aItems[ n ]
      hLinea := {=>}

  hLinea[ "NumeroLinea" ]           := n
  hLinea[ "DescripcionOperacion" ]  := aItem[ 2 ]  // descripcion
  hLinea[ "Cantidad" ]              := aItem[ 3 ]  // cantidad
  hLinea[ "PrecioUnitario" ]        := aItem[ 4 ]  // precio
  hLinea[ "ImporteTotal" ]          := Round( aItem[ 3 ] * aItem[ 4 ], 6 )
  hLinea[ "BaseImponibleOtributo" ] := Round( aItem[ 3 ] * aItem[ 4 ], 6 )

  // Impuesto
  hLinea[ "ClaveImpuesto" ]          := "01"  // IVA
  hLinea[ "ClaveRegimenEspecial" ]   := REG_GENERAL
  hLinea[ "TipoImpositivo" ]         := IVA_GENERAL
  hLinea[ "CuotaRepercutida" ]       := Round( aItem[ 3 ] * aItem[ 4 ] * IVA_GENERAL / 100, 6 )

  nBase += hLinea[ "BaseImponibleOtributo" ]
  nIva  += hLinea[ "CuotaRepercutida" ]

  AAdd( aLineas, hLinea )
   next

   nTotal := nBase + nIva + nRecargo

   // Cabecera
   hCabecera[ "IDREmisorFactura" ]      := cIdr

   hEmisor[ "NIFEmisor" ]              := ::cNif
   hEmisor[ "NombreRazonEmisor" ]       := ::cNombreRazon

   hDomicilio[ "DomicilioFiscal" ]      := ::cDomicilio
   hDomicilio[ "CodigoPostal" ]          := ::cCodigoPostal
   hDomicilio[ "Municipio" ]             := ::cMunicipio
   hDomicilio[ "Provincia" ]             := ::cProvincia
   hDomicilio[ "CodigoPais" ]            := ::cPais

   hEmisor[ "DomicilioEmisor" ]          := hDomicilio

   if ! Empty( ::cNIFRepresentante )
      hEmisor[ "NIFRepresentanteLegal" ] := ::cNIFRepresentante
   endif

   hCabecera[ "IDEmitente" ]            := hEmisor
   hCabecera[ "TipoFactura" ]           := If( Len( aItems ) <= 8, ;
                                             TIPO_SIMPLIF, TIPO_COMPLETA )
   hCabecera[ "TipoRectificativa" ]     := ""
   hCabecera[ "NumFactura" ]            := AllTrim( Str( ::nContadorFactura ) )
   hCabecera[ "SerieFactura" ]          := ::cSerieFactura
   hCabecera[ "FechaExpedicionFactura" ] := cFecha
   hCabecera[ "HoraExpedicionFactura" ] := cHora
   hCabecera[ "FacturadorSimplificado" ] := "S"

   // Datos de factura
   hDatos[ "DescripcionOperacion" ]     := ""
   hDatos[ "BaseImponible" ]            := Round( nBase, 6 )
   hDatos[ "CuotaRecargo" ]             := Round( nRecargo, 6 )
   hDatos[ "TotalFactura" ]             := Round( nTotal, 6 )
   hDatos[ "ImporteTotalPymes" ]        := Round( nTotal, 6 )

   hDatos[ "DatosRetencion" ]           := {=>}
   hDatos[ "DatosRetencion" ][ "BaseRetencion" ] := 0
   hDatos[ "DatosRetencion" ][ "PorcentajeRetencion" ] := 0
   hDatos[ "DatosRetencion" ][ "ImporteRetencion" ] := 0

   hDatos[ "LineasFactura" ]            := aLineas

   // Sistema VeriFactu
   cHuella := ::GenerarHuella( cIdr, ;
              AllTrim( Str( ::nContadorFactura ) ), ;
              ::cSerieFactura, cFecha, ;
              AllTrim( Trans( nBase, "@E 999999.999999" )), ;
              AllTrim( Trans( nRecargo, "@E 999999.999999" )), ;
              AllTrim( Trans( nTotal, "@E 999999.999999" )) )

   hSistema[ "Truncamiento" ]           := "N"
   hSistema[ "Encadenamiento" ]         := {=>}
   hSistema[ "Encadenamiento" ][ "PrimerRegistro" ] := ;
      If( Len( ::aRegistros ) == 0, "S", "N" )

   if Len( ::aRegistros ) > 0
      hSistema[ "Encadenamiento" ][ "IDRRegistroAnterior" ] := ;
         ::aRegistros[ Len( ::aRegistros ), "Cabecera", "IDREmisorFactura" ]
      hSistema[ "Encadenamiento" ][ "HuellaRegistroAnterior" ] := ;
         ::cUltimaHuella
   endif

   hSistema[ "SistemaUtilizado" ]       := "01"
   hSistema[ "Huella" ]                 := cHuella
   hSistema[ "VersionSCHEMA" ]          := "1.0"
   hSistema[ "VersionSOFTWARE" ]        := ::cSoftwareVersion
   hSistema[ "NombreSOFTWARE" ]         := ::cSoftwareName
   hSistema[ "NumeroSerialSoftware" ]   := StrZero( ::nContadorFactura, 10 )

   // Ensamblar registro
   hReg[ "Cabecera" ]      := hCabecera
   hReg[ "DatosFactura" ]  := hDatos
   hReg[ "SistemaVeriFactu" ] := hSistema

   // Metadata adicional
   hReg[ "_clienteNif" ]     := cClienteNif
   hReg[ "_clienteNombre" ]  := cClienteNombre
   hReg[ "_timestamp" ]      := DToC( Date() ) + " " + Time()
   hReg[ "_modo" ]           := If( ::lModoPrueba, "PRUEBA", "PRODUCCION" )

   // Guardar en registro
   AAdd( ::aRegistros, hReg )

return hReg

// ---------------------------------------------------------------------------

METHOD RegistroToXml( hReg ) CLASS VeriFactu

   local cXml := ""
   local hCab, hDatos, hSist, hEmis, hDom
   local aLineas, hLin, n

   if hReg == nil
      return ""
   endif

   hCab   := hReg[ "Cabecera" ]
   hDatos := hReg[ "DatosFactura" ]
   hSist  := hReg[ "SistemaVeriFactu" ]
   hEmis  := hCab[ "IDEmitente" ]
   hDom   := hEmis[ "DomicilioEmisor" ]

   cXml += '<?xml version="1.0" encoding="UTF-8"?>' + CRLF
   cXml += '<cf:RegistroFacturaSimplificado' + CRLF
   cXml += '   xmlns:cf="https://sede.agenciatributaria.gob.es/esquemas/verifactu"' + CRLF
   cXml += '   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' + CRLF

   // Cabecera
   cXml += '   <cf:Cabecera>' + CRLF
   cXml += '      <cf:IDREmisorFactura>' + AllTrim( hCab[ "IDREmisorFactura" ] ) + '</cf:IDREmisorFactura>' + CRLF
   cXml += '      <cf:IDEmitente>' + CRLF
   cXml += '         <cf:NIFEmisor>' + AllTrim( hEmis[ "NIFEmisor" ] ) + '</cf:NIFEmisor>' + CRLF
   cXml += '         <cf:NombreRazonEmisor>' + AllTrim( hEmis[ "NombreRazonEmisor" ] ) + '</cf:NombreRazonEmisor>' + CRLF
   cXml += '         <cf:DomicilioEmisor>' + CRLF
   cXml += '            <cf:DomicilioFiscal>' + AllTrim( hDom[ "DomicilioFiscal" ] ) + '</cf:DomicilioFiscal>' + CRLF
   cXml += '            <cf:CodigoPostal>' + AllTrim( hDom[ "CodigoPostal" ] ) + '</cf:CodigoPostal>' + CRLF
   cXml += '            <cf:Municipio>' + AllTrim( hDom[ "Municipio" ] ) + '</cf:Municipio>' + CRLF
   cXml += '            <cf:Provincia>' + AllTrim( hDom[ "Provincia" ] ) + '</cf:Provincia>' + CRLF
   cXml += '            <cf:CodigoPais>' + AllTrim( hDom[ "CodigoPais" ] ) + '</cf:CodigoPais>' + CRLF
   cXml += '         </cf:DomicilioEmisor>' + CRLF
   cXml += '      </cf:IDEmitente>' + CRLF
   cXml += '      <cf:TipoFactura>' + AllTrim( hCab[ "TipoFactura" ] ) + '</cf:TipoFactura>' + CRLF
   if ! Empty( hCab[ "TipoRectificativa" ] )
      cXml += '      <cf:TipoRectificativa>' + AllTrim( hCab[ "TipoRectificativa" ] ) + '</cf:TipoRectificativa>' + CRLF
   endif
   cXml += '      <cf:NumFactura>' + AllTrim( hCab[ "NumFactura" ] ) + '</cf:NumFactura>' + CRLF
   cXml += '      <cf:SerieFactura>' + AllTrim( hCab[ "SerieFactura" ] ) + '</cf:SerieFactura>' + CRLF
   cXml += '      <cf:FechaExpedicionFactura>' + AllTrim( hCab[ "FechaExpedicionFactura" ] ) + '</cf:FechaExpedicionFactura>' + CRLF
   cXml += '      <cf:HoraExpedicionFactura>' + AllTrim( hCab[ "HoraExpedicionFactura" ] ) + '</cf:HoraExpedicionFactura>' + CRLF
   cXml += '      <cf:FacturadorSimplificado>' + AllTrim( hCab[ "FacturadorSimplificado" ] ) + '</cf:FacturadorSimplificado>' + CRLF
   cXml += '   </cf:Cabecera>' + CRLF

   // Datos de factura
   cXml += '   <cf:DatosFactura>' + CRLF
   cXml += '      <cf:DescripcionOperacion>' + AllTrim( hDatos[ "DescripcionOperacion" ] ) + '</cf:DescripcionOperacion>' + CRLF
   cXml += '      <cf:BaseImponible>' + AllTrim( Trans( hDatos[ "BaseImponible" ], "@E 999999.999999" ) ) + '</cf:BaseImponible>' + CRLF
   cXml += '      <cf:CuotaRecargo>' + AllTrim( Trans( hDatos[ "CuotaRecargo" ], "@E 999999.999999" ) ) + '</cf:CuotaRecargo>' + CRLF
   cXml += '      <cf:TotalFactura>' + AllTrim( Trans( hDatos[ "TotalFactura" ], "@E 999999.999999" ) ) + '</cf:TotalFactura>' + CRLF
   cXml += '      <cf:ImporteTotalPymes>' + AllTrim( Trans( hDatos[ "ImporteTotalPymes" ], "@E 999999.999999" ) ) + '</cf:ImporteTotalPymes>' + CRLF

   // Lineas
   aLineas := hDatos[ "LineasFactura" ]
   for n := 1 to Len( aLineas )
      hLin := aLineas[ n ]
      cXml += '      <cf:FacturaLinea>' + CRLF
      cXml += '         <cf:NumeroLinea>' + AllTrim( Str( hLin[ "NumeroLinea" ] ) ) + '</cf:NumeroLinea>' + CRLF
      cXml += '         <cf:DescripcionOperacion>' + AllTrim( hLin[ "DescripcionOperacion" ] ) + '</cf:DescripcionOperacion>' + CRLF
      cXml += '         <cf:Cantidad>' + AllTrim( Trans( hLin[ "Cantidad" ], "@E 999999" ) ) + '</cf:Cantidad>' + CRLF
      cXml += '         <cf:PrecioUnitario>' + AllTrim( Trans( hLin[ "PrecioUnitario" ], "@E 999999.999999" ) ) + '</cf:PrecioUnitario>' + CRLF
      cXml += '         <cf:ImporteTotal>' + AllTrim( Trans( hLin[ "ImporteTotal" ], "@E 999999.999999" ) ) + '</cf:ImporteTotal>' + CRLF
      cXml += '         <cf:BaseImponibleOtributo>' + AllTrim( Trans( hLin[ "BaseImponibleOtributo" ], "@E 999999.999999" ) ) + '</cf:BaseImponibleOtributo>' + CRLF
      cXml += '         <cf:ImpuestoTransaccion>' + CRLF
      cXml += '            <cf:ClaveImpuesto>' + AllTrim( hLin[ "ClaveImpuesto" ] ) + '</cf:ClaveImpuesto>' + CRLF
      cXml += '            <cf:ClaveRegimenEspecial>' + AllTrim( hLin[ "ClaveRegimenEspecial" ] ) + '</cf:ClaveRegimenEspecial>' + CRLF
      cXml += '            <cf:TipoImpositivo>' + AllTrim( Trans( hLin[ "TipoImpositivo" ], "@E 99.99" ) ) + '</cf:TipoImpositivo>' + CRLF
      cXml += '            <cf:CuotaRepercutida>' + AllTrim( Trans( hLin[ "CuotaRepercutida" ], "@E 999999.999999" ) ) + '</cf:CuotaRepercutida>' + CRLF
      cXml += '         </cf:ImpuestoTransaccion>' + CRLF
      cXml += '      </cf:FacturaLinea>' + CRLF
   next

   cXml += '   </cf:DatosFactura>' + CRLF

   // Sistema VeriFactu
   cXml += '   <cf:SistemaVeriFactu>' + CRLF
   cXml += '      <cf:Truncamiento>' + AllTrim( hSist[ "Truncamiento" ] ) + '</cf:Truncamiento>' + CRLF
   cXml += '      <cf:Encadenamiento>' + CRLF
   cXml += '         <cf:PrimerRegistro>' + AllTrim( hSist[ "Encadenamiento" ][ "PrimerRegistro" ] ) + '</cf:PrimerRegistro>' + CRLF
   if hb_HHasKey( hSist[ "Encadenamiento" ], "IDRRegistroAnterior" )
      cXml += '         <cf:IDRRegistroAnterior>' + AllTrim( hSist[ "Encadenamiento" ][ "IDRRegistroAnterior" ] ) + '</cf:IDRRegistroAnterior>' + CRLF
      cXml += '         <cf:HuellaRegistroAnterior>' + AllTrim( hSist[ "Encadenamiento" ][ "HuellaRegistroAnterior" ] ) + '</cf:HuellaRegistroAnterior>' + CRLF
   endif
   cXml += '      </cf:Encadenamiento>' + CRLF
   cXml += '      <cf:SistemaUtilizado>' + AllTrim( hSist[ "SistemaUtilizado" ] ) + '</cf:SistemaUtilizado>' + CRLF
   cXml += '      <cf:Huella>' + AllTrim( hSist[ "Huella" ] ) + '</cf:Huella>' + CRLF
   cXml += '      <cf:VersionSCHEMA>' + AllTrim( hSist[ "VersionSCHEMA" ] ) + '</cf:VersionSCHEMA>' + CRLF
   cXml += '      <cf:VersionSOFTWARE>' + AllTrim( hSist[ "VersionSOFTWARE" ] ) + '</cf:VersionSOFTWARE>' + CRLF
   cXml += '      <cf:NombreSOFTWARE>' + AllTrim( hSist[ "NombreSOFTWARE" ] ) + '</cf:NombreSOFTWARE>' + CRLF
   cXml += '      <cf:NumeroSerialSoftware>' + AllTrim( hSist[ "NumeroSerialSoftware" ] ) + '</cf:NumeroSerialSoftware>' + CRLF
   cXml += '   </cf:SistemaVeriFactu>' + CRLF
   cXml += '</cf:RegistroFacturaSimplificado>' + CRLF

return cXml

// ---------------------------------------------------------------------------

METHOD EnviarAEAT( hReg ) CLASS VeriFactu

   local cXml, cResponse := ""
   local oHttp, oErr

   if ::lModoPrueba
      // En modo prueba, simular respuesta exitosa
      ::LogRegistro( hReg, "ENVIADO_PRUEBA" )
      return { "ok" => .T., "estado" => "PRUEBA", "mensaje" => "Registro generado (modo prueba)" }
   endif

   // Generar XML
   cXml := ::RegistroToXml( hReg )

   // Enviar a AEAT via HTTP POST
   BEGIN SEQUENCE
      oHttp := TIpClientHttp():New( ::cEndpoint )
      oHttp:setPostFields( cXml )
      oHttp:addHeader( "Content-Type", "application/xml" )
      oHttp:addHeader( "Accept", "application/xml" )

  // En produccion: agregar certificado digital
  // oHttp:setCertFile( ::cCertificadoRuta, ::cCertificadoPass )

  cResponse := oHttp:post()

  ::LogRegistro( hReg, "ENVIADO" )

  return { "ok" => .T., "estado" => "ENVIADO", "respuesta" => cResponse }

   RECOVER USING oErr
      ::LogRegistro( hReg, "ERROR: " + oErr:Description )
      return { "ok" => .F., "estado" => "ERROR", "mensaje" => oErr:Description }
   END

return { "ok" => .F., "estado" => "ERROR", "mensaje" => "Desconocido" }

// ---------------------------------------------------------------------------

METHOD Validar() CLASS VeriFactu

   local aErrores := {}

   if Empty( ::cNif )
      AAdd( aErrores, "NIF del emisor no configurado" )
   endif

   if Empty( ::cNombreRazon )
      AAdd( aErrores, "Nombre/Razon social no configurado" )
   endif

   if Empty( ::cDomicilio )
      AAdd( aErrores, "Domicilio no configurado" )
   endif

   if Empty( ::cCodigoPostal )
      AAdd( aErrores, "Codigo postal no configurado" )
   endif

   if Empty( ::cMunicipio )
      AAdd( aErrores, "Municipio no configurado" )
   endif

   if Empty( ::cProvincia )
      AAdd( aErrores, "Provincia no configurado" )
   endif

return aErrores

// ---------------------------------------------------------------------------

METHOD GetEstado() CLASS VeriFactu

   local hEstado := {=>}

   hEstado[ "modo" ]             := If( ::lModoPrueba, "PRUEBA", "PRODUCCION" )
   hEstado[ "nif" ]              := ::cNif
   hEstado[ "nombre" ]           := ::cNombreRazon
   hEstado[ "software" ]         := ::cSoftwareName + " v" + ::cSoftwareVersion
   hEstado[ "registros" ]        := Len( ::aRegistros )
   hEstado[ "ultimaHuella" ]     := ::cUltimaHuella
   hEstado[ "ultimoIDR" ]        := ::cUltimoIDR
   hEstado[ "serie" ]            := ::cSerieFactura
   hEstado[ "siguienteFactura" ] := ::nContadorFactura + 1
   hEstado[ "endpoint" ]         := ::cEndpoint
   hEstado[ "certificado" ]      := If( ! Empty( ::cCertificadoRuta ), "Configurado", "No configurado" )

return hEstado

// ---------------------------------------------------------------------------

METHOD LogRegistro( hReg, cEstado ) CLASS VeriFactu

   local cLinea

   cLinea := DToC( Date() ) + " " + Time() + " | " + ;
             AllTrim( hReg[ "Cabecera", "IDREmisorFactura" ] ) + " | " + ;
             "F" + AllTrim( hReg[ "Cabecera", "NumFactura" ] ) + ;
             "/" + AllTrim( hReg[ "Cabecera", "SerieFactura" ] ) + " | " + ;
             cEstado + " | " + ;
             "$" + AllTrim( Trans( hReg[ "DatosFactura", "TotalFactura" ], "@E 999,999.99" ) )

   LogFile( "verifactu.log", { cLinea } )

return nil

// ============================================================================
// FUNCIONES DE UTILIDAD
// ============================================================================

function VF_CrearEmisor( oVF, cNif, cNombre, cDomicilio, cCp, cMuni, cProv )

   oVF:Configurar( cNif, cNombre, cDomicilio, cCp, cMuni, cProv )

return oVF

// ---------------------------------------------------------------------------

function VF_Facturar( oVF, aItems, cNumFact, cClienteNif, cClienteNombre )

   local hReg, cXml, hResult
   local nSub := 0, nIva, nTotal
   local n

   // Calcular totales
   for n := 1 to Len( aItems )
      nSub += aItems[ n, 3 ] * aItems[ n, 4 ]
   next
   nIva   := Round( nSub * IVA_GENERAL / 100, 2 )
   nTotal := nSub + nIva

   // Crear registro VeriFactu
   hReg := oVF:CrearRegistro( aItems, ;
           { "subtotal" => nSub, "iva" => nIva, "total" => nTotal }, ;
           cNumFact, DToC( Date() ), Time(), ;
           cClienteNif, cClienteNombre )

   if hReg == nil
      return { "ok" => .F., "error" => "Error creando registro" }
   endif

   // Generar XML
   cXml := oVF:RegistroToXml( hReg )

   // Enviar a AEAT
   hResult := oVF:EnviarAEAT( hReg )

   hResult[ "xml" ] := cXml
   hResult[ "registro" ] := hReg

return hResult

// ---------------------------------------------------------------------------

function VF_GuardarXml( cXml, cRuta )

   local lOk := .F.

   BEGIN SEQUENCE
      MemoWrit( cRuta, cXml )
      lOk := .T.
   RECOVER
      lOk := .F.
   END

return lOk

// ---------------------------------------------------------------------------

function VF_Ejemplo()

   local oVF := VeriFactu():New()

   // Configurar emisor
   oVF:Configurar( ;
      "B12345678", ;
      "Ferreteria Constructora S.L.", ;
      "Calle Ejemplo 123", ;
      "28001", ;
      "Madrid", ;
      "Madrid" )

   oVF:cSoftwareName    := "CotizadorWH"
   oVF:cSoftwareVersion := "1.0.0"
   oVF:lModoPrueba      := .T.

   // Ejemplo de facturacion
   local aItems := { ;
      { "BROC0003", "Brocha 2 Pulgadas",          5, 3.20 }, ;
      { "PINT0001", "Pintura Blanca Interior 1 LT", 2, 12.50 }, ;
      { "ESPA0004", "Espatula 3 Pulgadas",         1, 4.75 } ;
   }

   local hResult := VF_Facturar( oVF, aItems, "001", "0101234567", "Juan Perez" )

   if hResult[ "ok" ]
      ? "VeriFactu: OK"
      ? "IDR:", hResult[ "registro" ], "Cabecera", "IDREmisorFactura"
      ? "Estado:", hResult[ "estado" ]
      ? "Modo:", hResult[ "registro" ], "_modo"
      ?
      ? "XML generado:"
      ? hResult[ "xml" ]
   else
      ? "VeriFactu ERROR:", hResult[ "error" ]
   endif

return nil
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 0
Joined: Wed Aug 26, 2026 09:41 AM

Re: Verifactu según "mimo"

Posted: Thu Aug 27, 2026 05:41 AM
**Antonio, gracias por compartir el apunte.**

El código que pega el hilo está **incompleto** (se corta en la declaración de `METHOD Configurar…`). Como bien dices, es solo una orientación generada por IA y **no está probado**.

Un par de observaciones rápidas sobre la estructura que se ve:

1. **Huella encadenada**
El campo `cUltimaHuella` y `cUltimoIDR` son correctos para la cadena de hash, pero recuerda que la AEAT exige que la huella se calcule **sobre el registro anterior ya firmado** (incluyendo su propia huella). Verifica el orden exacto de concatenación que publica la especificación técnica (XML `RegistroFacturaSimplificado` v1.0).

2. **Certificado y firma**
En `DATA cCertificadoRuta / cCertificadoPass` falta el método real de firma (XAdES-BES con certificado cualificado). En Harbour lo habitual es delegar en `openssl` o en la librería `hbcrypto` / `hbopenssl`; FiveWin no trae esa funcionalidad nativa.

3. **Envío HTTPS**
`hbsocket.ch` es bajo nivel. Para producción te ahorrarás dolores de cabeza usando `hbhttp` / `hbcurl` (POST con `Content-Type: application/xml` y certificado cliente).

4. **Tipos y regímenes**
Las constantes `#define` coinciden con las tablas publicadas, pero comprueba si tu caso usa factura **completa** (`A`) o **simplificada** (`S`), porque el XML cambia (p.ej. desglose de IVA obligatorio en completa).

5. **Log y contingencia**
`aRegistros` y `LogRegistro()` están bien para auditoría interna, pero ten en cuenta el **registro en local** obligatorio si la AEAT devuelve error o timeout (48 h para reintentar).

---

### ¿En qué punto necesitas ayuda concreta?

- Completar los métodos `GenerarHuella()`, `CrearRegistro()`, `RegistroToXml()`…
- Firma XAdES con `hbopenssl` / `openssl` CLI.
- Envío POST real y parseo de respuesta (`estado`, `csv`, `errores`).
- Pruebas contra el entorno **preproducción** (endpoint de prueba) antes de ir a producción.

Si me dices qué parte te bloquea, te paso un **ejemplo propio** (sin copiar el código de arriba) con el esqueleto mínimo que compila y firma contra el XSD oficial.

Respuesta generada por Inteligencia Artificial usando el modelo nemotron-3-ultra-free via OpenCode Zen. Puede contener errores; por favor verifiquela antes de aplicar.
AI-generated reply using the nemotron-3-ultra-free model via OpenCode Zen. May contain mistakes; please verify before applying.

Continue the discussion