FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin para Harbour/xHarbour Generaci贸n de C贸digo QR Veri*factu (AEAT)
Posts: 14
Joined: Fri Aug 01, 2025 08:40 PM
Generaci贸n de C贸digo QR Veri*factu (AEAT)
Posted: Mon Aug 11, 2025 08:19 PM
Hola a todos,

Comparto un ejemplo funcional en para la generaci贸n del C贸digo QR exigido por la AEAT en el marco de Veri*factu.
Este c贸digo muestra c贸mo construir la URL de validaci贸n (verificable y no verificable), codificar correctamente los par谩metros en UTF-8 mediante UrlEncode() y generar la imagen BMP del QR usando SimpleCodeGenerator.exe.

Incluye:
  • Ejemplos de facturas verificables y no verificables.
  • Funci贸n UrlEncode() compatible con UTF-8.
  • Funci贸n GeneraQRBmp()
  • Preparaci贸n de datos de ejemplo en CargaFra() para pruebas r谩pidas.
📌 Nota: El c贸digo se publica solo como ejemplo educativo, sin garant铆as para uso en producci贸n.
/*
   Ejemplo de generaci贸n de c贸digo QR AEAT - Veri*factu
   漏 2025 Biel Maim贸
*/
#include "FiveWin.ch"
#define VERI_ENV_PROD  .F.   // .T. = Producci贸n | .F. = Pruebas

//-----------------------------------------------------------------------------
FUNCTION Main(...)
   LOCAL hLisFra:={=>}, hFra, cId, cFile
   
   CargaFra(hLisFra) //Carga facturas en un hash, modificando esta funci贸n puede cargar de cualquier origen

   FOR EACH cId IN hLisFra:Keys()
      hFra=hLisFra[ cId ]
      FormatFra(hFra)
      hFra["QR"     ] := GeneraQRTxt( hFra )
      cFile := NombreArchivo( hFra )
      GeneraQrBmp( hFra["QR"], cFile )
      MsgInfo(hFra["QR"]," Factura "+hFra["serie"]+"|"+hFra["numero"])
   NEXT

RETURN NIL
//-----------------------------------------------------------------------------
STATIC FUNCTION GeneraQRTxt( hFra )
   LOCAL lVerif,cBase,cQr
   lVerif := IIF( HB_HHasKey( hFra, "verificable" ), hFra["verificable"], .F. )
   cBase  := QRaeatUrl( lVerif )
   cQr    := cBase +;
              "?nif="       + UrlEncode( hFra["nif"] ) + ;
              "&numserie=" + UrlEncode( hFra["serie"] + hFra["numero"] ) + ;
              "&fecha="    + UrlEncode( Transform( hFra["fecha"], "DD-MM-YYYY")  ) + ;
              "&importe="  + UrlEncode( _Dec2( Val( hFra["importe"] ) / 100 ) )
RETURN cQr

//-----------------------------------------------------------------------------
STATIC FUNCTION GeneraQRBmp( cQr, cFile, nScale )
   LOCAL cExe, cCommand
   hb_Default( @nScale, 5 )
   cExe := "SimpleCodeGenerator.exe"
   cFile+='.BMP'
   cQr := StrTran( cQr, '"', '\"' )
   cCommand := cExe + ' /ErrorCorrection 2 /save "' + cQr + '" "' + cFile + '" ' + LTrim(Str(nScale))
   FErase(cFile)
   hb_processRun( cCommand )
RETURN File( cFile ) //Verdadero si se ha creado el fichero
//------------------------------------------------------------------------------
STATIC FUNCTION QRAeatURL( lVerificable )
   LOCAL cUrl
   IF lVerificable
      cUrl:=IIF( VERI_ENV_PROD, ;
         "https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR", ;
         "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR" )
   ELSE
      cUrl:=IIF( VERI_ENV_PROD, ;
         "https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQRNoVerifactu", ;
         "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQRNoVerifactu" )
   ENDIF
RETURN cURl

//------------------------------------------------------------------------------
// Formatea un n煤mero como "21.00" sin ceros a la izquierda
STATIC FUNCTION _Dec2( nVal )
   LOCAL nCents, lNeg, nAbs, nInt, nFrac, cOut
   nCents := Int( nVal * 100 + IIF( nVal >= 0, 0.5, -0.5 ) )  
   lNeg   := ( nCents < 0 )  // Bandera para negativo
   nAbs   := Abs( nCents )   // Valor absoluto
   nInt   := Int( nAbs / 100 )  // Parte entera
   nFrac  := nAbs % 100         // Parte fraccionaria
   cOut   := ""

   IF lNeg
      cOut += "-"
   ENDIF

   cOut += LTrim( Str( nInt, 0 ) ) + "." + StrZero( nFrac, 2 )
RETURN cOut
//-----------------------------------------------------------------------------
STATIC FUNCTION CargaFra( hLisFra )
   LOCAL cID
   //--Fra 1
   cID := "Fra1"
   hLisFra[ cID ] := {=>}
   hLisFra[ cID ][ "nif"         ] := "B12345678"
   hLisFra[ cID ][ "serie"       ] := "F2025"
   hLisFra[ cID ][ "numero"      ] := 100
   hLisFra[ cID ][ "tipo"        ] := 'F1'
   hLisFra[ cID ][ "operacion"   ] := '01'
   hLisFra[ cID ][ "nifReceptor" ] := "12345678Z"
   hLisFra[ cID ][ "fecha"       ] := CTOD("02/08/2025")
   hLisFra[ cID ][ "horaUtc"     ] := '12:21:12'
   hLisFra[ cID ][ "importe"     ] := 135.32
   hLisFra[ cID ][ "desglose"    ] := {}
   hLisFra[ cID ][ "verificable" ] := .T.
   AddIva( hLisFra[cID],21,107.22,22.52,5.20,5.58)
   //--Fra 2
   cID := "Fra2"
   hLisFra[ cID ] := {=>}
   hLisFra[ cID ][ "nif"         ] := "B12345678"
   hLisFra[ cID ][ "serie"       ] := "F2025"
   hLisFra[ cID ][ "numero"      ] := 101
   hLisFra[ cID ][ "tipo"        ] := 'F1'
   hLisFra[ cID ][ "operacion"   ] := '01'
   hLisFra[ cID ][ "nifReceptor" ] := "43052326H"
   hLisFra[ cID ][ "fecha"       ] := CTOD("02/08/2025")
   hLisFra[ cID ][ "horaUtc"     ] := '15:14:00'
   hLisFra[ cID ][ "importe"     ] :=917.7
   hLisFra[ cID ][ "desglose"    ] := {}
   hLisFra[ cID ][ "verificable" ] := .F.
   AddIva( hLisFra[cID],21,805,169.05)
   AddIva( hLisFra[cID],10,452,45.2)
   AddIva( hLisFra[cID],21,805,169.05)
   //--Fra 3
   cID := "Fra3"
   hLisFra[ cID ] := {=>}
   hLisFra[ cID ][ "nif"         ] := "B12345678"
   hLisFra[ cID ][ "serie"       ] := "F2025"
   hLisFra[ cID ][ "numero"      ] := 102
   hLisFra[ cID ][ "tipo"        ] := 'F2'
   hLisFra[ cID ][ "operacion"   ] := '01'
   hLisFra[ cID ][ "nifReceptor" ] := ""
   hLisFra[ cID ][ "fecha"       ] := CTOD("03/08/2025")
   hLisFra[ cID ][ "horaUtc"     ] := '20:20:12'
   hLisFra[ cID ][ "importe"     ] := 77.44
   hLisFra[ cID ][ "desglose"    ] := {}
   hLisFra[ cID ][ "verificable" ] := .T.
   AddIva( hLisFra[cID],21,64,13.44)
RETURN NIL
//------------------------------------------------------------------------------
STATIC FUNCTION AddIva( hFra, nTipo, nBase, nCuotaIva, nTipoR, nCuotaR )
   LOCAL h,lAdd
   h:=FindTipo( hFra, nTipo )
   lAdd:=IIf(h[ "tipo"  ]==0,.T.,.F.)
   hb_Default( @nTipoR, 0)
   h[ "tipo"  ] := nTipo
   h[ "base" ]  += nBase
   h[ "cuotaIva"]  += nCuotaIva
   IF nTipoR> 0
      h[ "TipoR"     ] := nTipoR
      h[ "CuotaR"    ] += nCuotaR
   ENDIF
   IF lAdd
      AAdd( hFra[ "desglose" ], h )
   ENDIF
RETURN NIL
//------------------------------------------------------------------------------
STATIC FUNCTION FindTipo( hFra, nTipo )
   LOCAL i, h
   FOR i := 1 TO Len( hFra["desglose"] )
      IF hFra["desglose"][i]["tipo"] == nTipo
         h:=hFra["desglose"][i]
         EXIT
      ENDIF
   NEXT
   IF h==NIL
      h:={=>}
      h[ "tipo"    ] := 0
      h[ "base"    ] := 0
      h[ "cuotaIva"] := 0
      h[ "TipoR"   ] := 0
      h[ "CuotaR"  ] := 0
   ENDIF
RETURN h
//-----------------------------------------------------------------------------
STATIC FUNCTION FormatFra( hFra )
   hFra["numero"]   := StrZero( hFra["numero"], 10 )
   // Fecha ISO (YYYY-MM-DD)
   hFra["fechaISO"] := Str( Year( hFra["fecha"] ), 4 ) + "-" + ;
                     StrZero( Month( hFra["fecha"] ), 2 ) + "-" + ;
                     StrZero( Day( hFra["fecha"] ), 2 )
   // Importe en c茅ntimos como entero de 12 d铆gitos
   hFra["importe"]  := STRZERO( INT( hFra["importe"] * 100 ), 12 )
RETURN NIL
//-----------------------------------------------------------------------------
FUNCTION NombreArchivo( hFra )
   RETURN AllTrim( hFra["nif"   ] ) + "_" + ;
          AllTrim( hFra["serie" ] ) + "_" + ;
          AllTrim( hFra["numero"] ) + "_" + ;
          DToS( hFra["fecha"])

//------------------------------------------------------------------------------
STATIC FUNCTION UrlEncode( cIn )
   LOCAL nLen,cOut,nI,cChar,cHex

   IF ! HB_IsString( cIn )
      RETURN ""
   ENDIF
   cIn   := hb_StrToUTF8( cIn )
   nLen  := Len( cIn )
   cOut  := ""
   cHex  := "0123456789ABCDEF"

   FOR nI := 1 TO nLen
      cChar := SubStr( cIn, nI, 1 )
      IF IsUnreserved( cChar )
         cOut += cChar
      ELSE
         cOut += "%" + ;
                 SubStr( cHex, Int( Asc( cChar ) / 16 ) + 1, 1 ) + ;
                 SubStr( cHex, ( Asc( cChar ) % 16 ) + 1, 1 )
      ENDIF
   NEXT

RETURN cOut
//-----------------------------------------------------------------------------
STATIC FUNCTION IsUnreserved( cChar )
RETURN ( cChar >= "A" .AND. cChar <= "Z" ) .OR. ;
       ( cChar >= "a" .AND. cChar <= "z" ) .OR. ;
       ( cChar >= "0" .AND. cChar <= "9" ) .OR. ;
       cChar == "-" .OR. cChar == "_" .OR. cChar == "." .OR. cChar == "~"
Espero que sea 煤til para quien est茅 implementando Veri*factu o necesite entender el formato del QR de la AEAT.
Cualquier mejora, duda o sugerencia es bienvenida.

Un abrazo,
Biel Maim贸

Continue the discussion