EET 2.0 tchèque en Delphi : enregistrer les ventes avec TsgcEETClient

· Composants
EET 2.0 tchèque en Delphi : enregistrer les ventes avec TsgcEETClient | Blog eSeGeCe

La République tchèque rétablit l'enregistrement électronique des ventes. Avec EET 2.0 (Elektronická evidence tržeb), un point de vente déclare chaque vente à l'administration fiscale au moment où elle a lieu, et l'administration fiscale répond par un code d'accusé de réception, le pok, qui prouve que la vente a été déclarée. La déclaration débute le 1er janvier 2027, et l'environnement playground dans lequel les caisses sont développées et testées est déjà ouvert.

sgcSign propose un nouveau composant pour cela, TsgcEETClient. Il valide la vente, construit le message, le signe avec le certificat du contribuable, l'envoie, contrôle la signature de l'accusé de réception et renvoie le résultat. Cet article explique ce qu'exige EET 2.0, comment le composant effectue l'aller-retour, et présente le code Delphi pour une première vente, une file d'attente hors ligne et un accusé de réception vérifié.

Une vente enregistrée depuis la démo Delphi, du mode vérification jusqu'à un vrai pok. Également sur YouTube.

EET 2.0 est un nouveau protocole, pas une mise à jour

Si tu as développé une caisse pour le premier dispositif EET, repars de zéro. La version 4.1 de l'interface de données n'est pas compatible avec l'ancienne version 3.1, et elle est plus simple : aucun code de sécurité PKP ou BKP à calculer, aucune ventilation de la TVA et aucun certificat client TLS. Une vente se résume à dix attributs de données. Il reste un service web SOAP 1.1 standard :

L'inscription en tant que contribuable, l'obtention du certificat via le portail MOJE daně et l'attribution d'un numéro d'unité d'enregistrement ont toutes lieu avant qu'une seule ligne de code ne s'exécute. De toutes ces démarches, la bibliothèque n'a besoin que d'un fichier PKCS#12 et de deux numéros, l'identifiant du contribuable et l'identifiant de l'unité.

Comment TsgcEETClient effectue l'aller-retour

Un seul appel à Send exécute chaque étape, dans cet ordre :

  1. Valide chaque champ de l'enregistrement TsgcEETSale selon les règles du schéma, de sorte qu'une vente invalide est refusée localement avec un motif lisible et n'atteint jamais le service.
  2. Construit l'élément Trzba et l'encapsule dans une enveloppe SOAP 1.1, avec un nouvel uuid_zpravy pour le message.
  3. Signe le corps SOAP selon WS-Security avec la clé de n'importe quel fournisseur de clés sgcSign : un fichier PFX, le magasin de certificats Windows, un token PKCS#11 ou une carte à puce, ou un service de clés cloud.
  4. Mesure l'enveloppe finale par rapport au plafond de 12 kB avant tout envoi.
  5. L'envoie à l'administration fiscale. L'environnement playground est l'endpoint par défaut, de sorte qu'un composant déposé sur une fiche ne peut pas déclarer une vraie vente par accident.
  6. Analyse la réponse : le pok, l'heure de réception, l'indicateur de test, les avertissements et le code d'erreur.
  7. Vérifie la signature de l'accusé de réception. Les réponses d'erreur ne sont pas signées par conception, donc un rejet ne se transforme jamais en échec de signature.

Ta première vente en Delphi

La spécification demande aux contribuables de commencer par le mode vérification. Le message est contrôlé intégralement, exactement comme un vrai, puis écarté, donc rien n'est déclaré. S'il passe, le certificat, la signature, la connexion TLS et chaque champ de la vente sont corrects. Le code ci-dessous effectue d'abord ce contrôle, puis déclare la vente pour de bon.

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;

Quelques détails décident de la réussite de ce premier essai :

Pour l'environnement playground, l'administration fiscale publie des certificats de test partagés, dont CZ00000019, sur eet.gov.cz. Un accusé de réception du playground contient test="true" et un pok se terminant par ff, ce qui ne prouve rien pour une vraie vente.

Lire la réponse

C'est la partie du protocole la plus susceptible de te surprendre. Chaque résultat arrive en HTTP 200, rejet compris, donc le statut HTTP ne t'apprend rien. Et un succès en mode vérification arrive dans un élément d'erreur portant le code 0, donc TsgcEETResponse.IsError vaut True lors d'une vérification parfaitement réussie.

sgcEETResponseOutcome applique ces deux règles et renvoie l'une de trois réponses :

RésultatSignification
eoAcknowledgedLa vente est déclarée et le pok se trouve dans TsgcEETResponse.POK. C'est le seul résultat qui déclare une vente.
eoVerifiedLe succès du mode vérification. Rien n'a été déclaré.
eoRejectedTout le reste. La vente n'a pas été déclarée et reste due à l'administration fiscale.

Les avertissements ne sont pas critiques. Jusqu'à dix d'entre eux peuvent accompagner un accusé de réception valide, et OnWarning se déclenche une fois pour chacun, tandis que OnError se déclenche pour une erreur dont le code n'est pas 0. Après chaque message, journalise LastTransactionId, l'en-tête de réponse X-Global-Transaction-Id, car c'est la première chose que demande le support EET. LastRequestXML et LastResponseXML conservent les deux messages exactement tels qu'ils ont circulé.

Quand la ligne est coupée : une file d'attente hors ligne

Une caisse doit continuer à vendre quand la connexion tombe. TsgcEETClient découpe l'aller-retour pour qu'un point de vente puisse mettre des messages en file d'attente et les envoyer plus tard :

// 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);

Un piège mérite d'être connu avant de construire la file d'attente. L'heure de la vente est écrite avec un décalage de fuseau horaire, et à moins que l'enregistrement de la vente ne porte son propre décalage, la bibliothèque utilise le décalage de la machine au moment où le message est construit. Une vente de juillet renvoyée avec Resend en décembre serait horodatée avec le décalage de décembre. Stocke le décalage avec la vente, et définis SaleOffsetMinutes et HasSaleOffsetMinutes quand tu la renvoies.

Vérifier l'accusé de réception

VerifyResponseSignature vaut True par défaut, donc la signature de chaque accusé de réception est contrôlée d'emblée. Contrôler aussi la chaîne de certificats demande les bonnes ancres de confiance, et ce ne sont pas celles auxquelles on pense. L'accusé de réception est signé par un certificat commercial I.CA, et non par les certificats EET fournis avec le matériel de test du playground, et aucun des deux émetteurs I.CA ne figure dans le magasin racine de Windows. Télécharge I.CA Root CA/RSA 05/2022 et I.CA Public CA/RSA 06/2022 depuis ica.cz et déclare-les comme ancres :

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

Si un contrôle échoue, LastVerificationDetails indique l'étape en échec. Quand un accusé de réception arrive mais que sa signature ne se vérifie pas, Send lève une exception, et LastResponse contient toujours la réponse analysée avec son pok, de sorte qu'une vente déjà enregistrée n'est jamais envoyée deux fois par erreur.

Le plafond de 12 kB

Le service refuse un message de plus de 12 kB avec le code d'erreur 7, et BuildMessage vérifie la taille avant tout envoi. Chaque champ de la vente est plafonné par le schéma, donc la seule partie de l'enveloppe dont la taille varie réellement est le certificat de signature dans le wsse:BinarySecurityToken. C'est aussi pourquoi l'enveloppe porte exactement un en-tête SOAP, et pourquoi le composant ne permet pas d'en ajouter un autre.

C++Builder, .NET, le serveur et la ligne de commande

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

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

Essaie-le

La démo Delphi dans Demos\Delphi\EET parcourt tout l'aller-retour avec l'environnement playground sur une seule fiche. Charge un certificat de test, envoie en mode vérification, puis décoche-le et envoie une vraie vente pour obtenir un accusé de réception avec son pok. Build Message (no send) affiche l'enveloppe signée qu'une file d'attente hors ligne stockerait, et Resend Stored Sale renvoie la dernière vente comme une soumission répétée. Les certificats de test ne sont pas inclus avec la démo, car le document qui les distribue est à diffusion restreinte, télécharge-les donc depuis eet.gov.cz.

Chaque propriété, méthode et événement est documenté dans l'aide en ligne de sgcSign, et la section EET 2.0 de la page des profils par pays de sgcSign résume le composant.

Disponibilité

TsgcEETClient est livré dans sgcSign 2026.10 pour Delphi, C++Builder et .NET, avec la route sgcSign Server et le verbe eet de l'outil en ligne de commande.

Des questions, ou une caisse qui doit être prête pour janvier ? Contacte-nous. Si quelque chose ne se comporte pas comme prévu, envoie le XML de la requête et de la réponse avec le X-Global-Transaction-Id, et tu obtiendras une réponse des personnes qui ont écrit le code.