EET 2.0 checo en Delphi: registra ventas con TsgcEETClient

· Componentes
EET 2.0 checo en Delphi: registra ventas con TsgcEETClient

La República Checa recupera el registro electrónico de ventas. Con EET 2.0 (Elektronická evidence tržeb) un punto de venta comunica cada venta a la autoridad tributaria en el momento en que se produce, y la autoridad tributaria responde con un código de acuse de recibo, el pok, que es la prueba de que la venta se ha comunicado. La comunicación de ventas comienza el 1 de enero de 2027, y el entorno de pruebas (playground) donde se construyen y prueban las cajas registradoras ya está abierto.

sgcSign incorpora un nuevo componente para ello, TsgcEETClient. Valida la venta, construye el mensaje, lo firma con el certificado del contribuyente, lo envía, comprueba la firma del acuse de recibo y devuelve el resultado. Esta publicación explica qué exige EET 2.0, cómo realiza el componente el ciclo completo y el código Delphi para una primera venta, una cola sin conexión y un acuse de recibo verificado.

Una venta registrada desde la demo de Delphi, desde el modo de verificación hasta un pok real. También en YouTube.

EET 2.0 es un protocolo nuevo, no una actualización

Si construiste una caja registradora para el primer sistema EET, empieza desde cero. La versión 4.1 de la interfaz de datos no es compatible con la antigua versión 3.1, y es más sencilla: no hay código de seguridad PKP ni BKP que calcular, ni desglose de IVA, ni certificado de cliente TLS. Una venta son diez atributos de datos. Lo que queda es un servicio web SOAP 1.1 estándar:

Darse de alta como contribuyente, obtener el certificado a través del portal MOJE daně y recibir un número de establecimiento asignado son trámites que se hacen antes de ejecutar cualquier código. De todo ese papeleo la biblioteca necesita un archivo PKCS#12 y dos números, el identificador del contribuyente y el identificador del establecimiento.

Cómo realiza TsgcEETClient el ciclo completo

Una llamada a Send ejecuta todos los pasos, en este orden:

  1. Valida cada campo del registro TsgcEETSale conforme a las reglas del esquema, de modo que una venta no válida se rechaza localmente con un motivo legible y nunca llega al servicio.
  2. Construye el elemento Trzba y lo envuelve en un sobre SOAP 1.1, con un uuid_zpravy nuevo para el mensaje.
  3. Firma el cuerpo SOAP con WS-Security usando la clave de cualquier proveedor de claves de sgcSign: un archivo PFX, el almacén de certificados de Windows, un token PKCS#11 o una tarjeta inteligente, o un servicio de claves en la nube.
  4. Mide el sobre terminado frente al límite de 12 kB antes de enviar nada.
  5. Lo envía a la autoridad tributaria. El entorno de pruebas es el endpoint predeterminado, de modo que un componente colocado en un formulario no puede presentar una venta real por accidente.
  6. Analiza la respuesta: el pok, la hora de recepción, el indicador de prueba, las advertencias y el código de error.
  7. Verifica la firma del acuse de recibo. Las respuestas de error no están firmadas por diseño, así que un rechazo nunca se convierte en un fallo de firma.

Tu primera venta en Delphi

La especificación indica a los contribuyentes que empiecen con el modo de verificación. El mensaje se comprueba por completo, exactamente igual que uno real, y después se descarta, de modo que no se presenta nada. Si supera la comprobación, el certificado, la firma, la conexión TLS y todos los campos de la venta son correctos. El código siguiente ejecuta primero esa comprobación y después presenta la venta de verdad.

var
  oProvider: TsgcPFXKeyProvider;
  oClient: TsgcEETClient;
  oSale: TsgcEETSale;
  oResponse: TsgcEETResponse;
begin
  oProvider := TsgcPFXKeyProvider.Create(nil);
  oClient := TsgcEETClient.Create(nil);
  try
    oProvider.FileName := 'CZ00000019.p12';
    oProvider.Password := '...';
    // Without LoadFromFile the certificate is empty and the message would
    // carry no token for the tax authority to verify the signature with.
    oProvider.LoadFromFile;
    oClient.KeyProvider := oProvider as IsgcKeyProvider;
    oClient.Environment := eetPlayground;

    sgcEETInitSale(oSale);
    oSale.SendDateTime := Now;
    oSale.SaleDateTime := Now;
    oSale.FirstSending := True;
    // The common name of an EET certificate IS the taxpayer identifier.
    oSale.TaxpayerEIC := oProvider.Certificate.SubjectCN;
    oSale.UnitID := 11;
    oSale.PosID := '1';
    oSale.ReceiptNumber := '0/6460/ZQ42';
    oSale.TotalAmount := 349;

    // Verification mode first. Nothing is filed.
    oClient.VerificationMode := True;
    oResponse := oClient.Send(oSale);
    if sgcEETResponseOutcome(oResponse) <> eoVerified then
      raise Exception.CreateFmt('Verification failed, code %d: %s',
        [oResponse.ErrorCode, oResponse.ErrorText]);

    // Now for real. Only eoAcknowledged reports a sale.
    oClient.VerificationMode := False;
    oResponse := oClient.Send(oSale);
    if sgcEETResponseOutcome(oResponse) = eoAcknowledged then
      PrintReceipt(oResponse.POK, oResponse.Test) // your own routine
    else
      // Not filed. Store the sale and replay it later with Resend.
      QueueSale(oSale); // your own routine
  finally
    oClient.Free;
    oProvider.Free;
  end;
end;

Unos pocos detalles deciden si esa primera ejecución funciona:

Para el entorno de pruebas, la administración tributaria publica certificados de prueba compartidos, entre ellos CZ00000019, en eet.gov.cz. Un acuse de recibo del entorno de pruebas incluye test="true" y un pok que termina en ff, lo que no demuestra nada sobre una venta real.

Lectura de la respuesta

Esta es la parte del protocolo que más probablemente te sorprenda. Todos los resultados llegan como HTTP 200, incluido un rechazo, así que el estado HTTP no te dice nada. Y un éxito en modo de verificación llega dentro de un elemento de error con el código 0, de modo que TsgcEETResponse.IsError es True en una ejecución de verificación perfectamente correcta.

sgcEETResponseOutcome aplica ambas reglas y devuelve una de tres respuestas:

ResultadoSignificado
eoAcknowledgedLa venta está comunicada y el pok está en TsgcEETResponse.POK. Es el único resultado que comunica una venta.
eoVerifiedEl éxito del modo de verificación. No se ha presentado nada.
eoRejectedTodo lo demás. La venta no se ha comunicado y sigue pendiente de comunicar a la autoridad tributaria.

Las advertencias no son críticas. Hasta diez pueden acompañar a un acuse de recibo válido, y OnWarning se dispara una vez por cada una, mientras que OnError se dispara ante un error cuyo código no es 0. Después de cada mensaje, registra LastTransactionId, la cabecera de respuesta X-Global-Transaction-Id, porque es lo primero que pide el soporte de EET. LastRequestXML y LastResponseXML conservan ambos mensajes exactamente como viajaron.

Cuando la línea está caída: una cola sin conexión

Una caja registradora tiene que seguir vendiendo cuando se cae la conexión. TsgcEETClient divide el ciclo completo para que un punto de venta pueda poner mensajes en cola y enviarlos más tarde:

// The line is down: sign the message now and keep it
sEnvelope := oClient.BuildMessage(oSale);
StoreInQueue(oClient.LastMessageUUID, sEnvelope); // your own storage

// The line is back: post the stored envelope exactly as it was built
oResponse := oClient.SendRaw(sEnvelope);

// Sent earlier but no answer arrived: replay the sale as a repeat
oResponse := oClient.Resend(oSale);

Conviene conocer una trampa antes de construir la cola. La hora de la venta se escribe con un desplazamiento de zona horaria, y si el registro de la venta no lleva su propio desplazamiento, la biblioteca usa el desplazamiento de la máquina en el momento en que se construye el mensaje. Una venta de julio repetida con Resend en diciembre quedaría marcada con el desplazamiento de diciembre. Guarda el desplazamiento junto con la venta, y asigna SaleOffsetMinutes y HasSaleOffsetMinutes cuando la repitas.

Verificación del acuse de recibo

VerifyResponseSignature es True por defecto, así que la firma de cada acuse de recibo se comprueba de serie. Comprobar también la cadena de certificados requiere los anclajes de confianza adecuados, y no son los evidentes. El acuse de recibo lo firma un certificado comercial de I.CA, no los certificados EET que acompañan al material de prueba del entorno de pruebas, y ninguno de los dos emisores de I.CA está en el almacén raíz de Windows. Descarga I.CA Root CA/RSA 05/2022 e I.CA Public CA/RSA 06/2022 desde ica.cz e indícalos como anclajes:

oClient.TrustedCertificates.Add('ica-root-ca-rsa-05-2022.cer');
oClient.TrustedCertificates.Add('ica-public-ca-rsa-06-2022.cer');
oClient.RequireTrustedChain := True;

Si una comprobación falla, LastVerificationDetails indica el paso que ha fallado. Cuando llega un acuse de recibo pero su firma no se verifica, Send lanza una excepción, y LastResponse sigue conteniendo la respuesta analizada con su pok, de modo que una venta ya registrada nunca se envía dos veces por error.

El límite de 12 kB

El servicio rechaza un mensaje de más de 12 kB con el código de error 7, y BuildMessage comprueba el tamaño antes de enviar nada. El esquema limita cada campo de la venta, así que la única parte del sobre cuyo tamaño varía realmente es el certificado de firma incluido en el wsse:BinarySecurityToken. Por eso también el sobre lleva exactamente una cabecera SOAP, y por eso el componente no ofrece ninguna forma de añadir otra.

C++Builder, .NET, el servidor y la línea de comandos

set SGCSIGN_SERVER=https://sign.shop.local:8443
set SGCSIGN_APIKEY=sgcsk_...

sgcsign eet --provider eet-taxpayer --submit sale.json

Pruébalo

La demo de Delphi en Demos\Delphi\EET recorre el ciclo completo contra el entorno de pruebas en un solo formulario. Carga un certificado de prueba, envía en modo de verificación, después desmarca esa opción y envía una venta real para obtener un acuse de recibo con su pok. Build Message (no send) muestra el sobre firmado que almacenaría una cola sin conexión, y Resend Stored Sale repite la última venta como un reenvío. Los certificados de prueba no se incluyen con la demo, porque el documento que los distribuye es de acceso restringido, así que descárgalos de eet.gov.cz.

Todas las propiedades, métodos y eventos están documentados en la ayuda en línea de sgcSign, y la sección EET 2.0 de la página de perfiles por país de sgcSign resume el componente.

Disponibilidad

TsgcEETClient se incluye en sgcSign 2026.10 para Delphi, C++Builder y .NET, junto con la ruta de sgcSign Server y el verbo eet de la herramienta de línea de comandos.

¿Preguntas, o una caja registradora que tiene que estar lista para enero? Ponte en contacto. Si algo no se comporta como esperas, envía el XML de la petición y de la respuesta junto con el X-Global-Transaction-Id, y recibirás respuesta de las personas que escribieron el código.