Tschechische EET 2.0 in Delphi: Verkäufe mit TsgcEETClient melden

· Komponenten
Tschechische EET 2.0 in Delphi: Verkäufe mit TsgcEETClient melden

Tschechien führt die elektronische Umsatzerfassung wieder ein. Mit EET 2.0 (Elektronická evidence tržeb) meldet eine Verkaufsstelle jeden Verkauf in dem Moment, in dem er stattfindet, an die Steuerbehörde, und die Steuerbehörde antwortet mit einem Bestätigungscode, dem pok, der als Nachweis dafür dient, dass der Verkauf gemeldet wurde. Die Meldungen beginnen am 1. Januar 2027, und der Playground, auf dem Kassen entwickelt und getestet werden, ist bereits geöffnet.

sgcSign hat dafür eine neue Komponente, TsgcEETClient. Sie validiert den Verkauf, erstellt die Nachricht, signiert sie mit dem Zertifikat des Steuerpflichtigen, sendet sie, prüft die Signatur der Bestätigung und gibt das Ergebnis zurück. Dieser Beitrag erklärt, was EET 2.0 verlangt und wie die Komponente den kompletten Ablauf abwickelt, und zeigt den Delphi-Code für einen ersten Verkauf, eine Offline-Warteschlange und eine verifizierte Bestätigung.

Ein erfasster Verkauf aus der Delphi-Demo, vom Verifizierungsmodus bis zum echten pok. Auch auf YouTube.

EET 2.0 ist ein neues Protokoll, kein Update

Wenn du eine Kasse für das erste EET-Verfahren gebaut hast, fang bei null an. Version 4.1 der Datenschnittstelle ist nicht mit der alten Version 3.1 kompatibel, und sie ist einfacher: Es gibt keinen PKP- oder BKP-Sicherheitscode zu berechnen, keine Aufschlüsselung der Mehrwertsteuer und kein TLS-Client-Zertifikat. Ein Verkauf besteht aus zehn Datenattributen. Übrig bleibt ein gewöhnlicher SOAP 1.1-Webservice:

Die Registrierung als Steuerpflichtiger, der Bezug des Zertifikats über das Portal MOJE daně und die Zuweisung einer Nummer der Betriebsstätte erfolgen, bevor irgendein Code läuft. Aus diesem Papierkram braucht die Bibliothek eine PKCS#12-Datei und zwei Nummern, die Kennung des Steuerpflichtigen und die Kennung der Betriebsstätte.

Wie TsgcEETClient den Ablauf abwickelt

Ein einziger Aufruf von Send führt alle Schritte aus, in dieser Reihenfolge:

  1. Validiert jedes Feld des TsgcEETSale-Records gegen die Schemaregeln, sodass ein ungültiger Verkauf lokal mit einer verständlichen Begründung abgelehnt wird und den Dienst nie erreicht.
  2. Erstellt das Element Trzba und verpackt es in einen SOAP 1.1-Envelope, mit einer neuen uuid_zpravy für die Nachricht.
  3. Signiert den SOAP-Body nach WS-Security mit dem Schlüssel eines beliebigen sgcSign Key Providers: einer PFX-Datei, dem Windows-Zertifikatspeicher, einem PKCS#11-Token oder einer Smartcard, oder einem Cloud-Schlüsseldienst.
  4. Misst den fertigen Envelope an der Obergrenze von 12 kB, bevor irgendetwas gesendet wird.
  5. Sendet ihn an die Steuerbehörde. Der Playground ist der Standardendpunkt, sodass eine auf ein Formular gezogene Komponente nicht versehentlich einen echten Verkauf melden kann.
  6. Wertet die Antwort aus: den pok, den Empfangszeitpunkt, das Test-Flag, die Warnungen und den Fehlercode.
  7. Prüft die Signatur der Bestätigung. Fehlerantworten sind absichtlich unsigniert, sodass eine Ablehnung nie zu einem Signaturfehler wird.

Dein erster Verkauf in Delphi

Die Spezifikation weist Steuerpflichtige an, mit dem Verifizierungsmodus zu beginnen. Die Nachricht wird vollständig geprüft, genau wie eine echte, und dann verworfen, es wird also nichts gemeldet. Besteht sie die Prüfung, sind das Zertifikat, die Signatur, die TLS-Verbindung und jedes Feld des Verkaufs korrekt. Der folgende Code führt zuerst diese Prüfung aus und meldet den Verkauf dann tatsächlich.

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;

Ein paar Details entscheiden darüber, ob dieser erste Lauf funktioniert:

Für den Playground veröffentlicht die Steuerverwaltung gemeinsam genutzte Testzertifikate, darunter CZ00000019, auf eet.gov.cz. Eine Bestätigung aus dem Playground enthält test="true" und einen pok, der auf ff endet, und beweist nichts über einen echten Verkauf.

Die Antwort auslesen

Dieser Teil des Protokolls wird dich am ehesten überraschen. Jedes Ergebnis kommt als HTTP 200 an, auch eine Ablehnung, der HTTP-Status sagt dir also nichts. Und ein Erfolg im Verifizierungsmodus kommt in einem Fehlerelement mit dem Code 0 an, sodass TsgcEETResponse.IsError bei einem völlig korrekten Verifizierungslauf True ist.

sgcEETResponseOutcome wendet beide Regeln an und liefert eines von drei Ergebnissen:

ErgebnisBedeutung
eoAcknowledgedDer Verkauf ist gemeldet, und der pok steht in TsgcEETResponse.POK. Dies ist das einzige Ergebnis, das einen Verkauf meldet.
eoVerifiedDer Erfolg im Verifizierungsmodus. Es wurde nichts gemeldet.
eoRejectedAlles andere. Der Verkauf wurde nicht gemeldet und muss der Steuerbehörde weiterhin gemeldet werden.

Warnungen sind nicht kritisch. Bis zu zehn davon können eine gültige Bestätigung begleiten, und OnWarning wird für jede einmal ausgelöst, während OnError bei einem Fehler ausgelöst wird, dessen Code nicht 0 ist. Protokolliere nach jeder Nachricht LastTransactionId, den Response-Header X-Global-Transaction-Id, denn danach fragt der EET-Support zuerst. LastRequestXML und LastResponseXML bewahren beide Nachrichten genau so auf, wie sie übertragen wurden.

Wenn die Verbindung ausfällt: eine Offline-Warteschlange

Eine Kasse muss weiter verkaufen können, wenn die Verbindung abbricht. TsgcEETClient teilt den Ablauf auf, sodass eine Verkaufsstelle Nachrichten in eine Warteschlange stellen und später senden kann:

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

Eine Falle solltest du kennen, bevor du die Warteschlange baust. Die Verkaufszeit wird mit einem Zeitzonen-Offset geschrieben, und sofern der Verkaufsdatensatz keinen eigenen Offset mitführt, verwendet die Bibliothek den Offset des Rechners zu dem Zeitpunkt, an dem die Nachricht erstellt wird. Ein Verkauf aus dem Juli, der im Dezember mit Resend wiederholt wird, würde mit dem Dezember-Offset versehen. Speichere den Offset zusammen mit dem Verkauf und setze SaleOffsetMinutes und HasSaleOffsetMinutes, wenn du ihn wiederholst.

Die Bestätigung verifizieren

VerifyResponseSignature ist standardmäßig True, sodass die Signatur jeder Bestätigung von Haus aus geprüft wird. Um zusätzlich die Zertifikatskette zu prüfen, braucht es die richtigen Vertrauensanker, und das sind nicht die naheliegenden. Die Bestätigung ist mit einem kommerziellen I.CA-Zertifikat signiert, nicht mit den EET-Zertifikaten aus dem Testmaterial des Playgrounds, und keiner der beiden I.CA-Aussteller ist im Windows-Stammzertifikatspeicher enthalten. Lade I.CA Root CA/RSA 05/2022 und I.CA Public CA/RSA 06/2022 von ica.cz herunter und gib sie als Anker an:

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

Schlägt eine Prüfung fehl, meldet LastVerificationDetails den fehlgeschlagenen Schritt. Kommt eine Bestätigung an, deren Signatur sich nicht verifizieren lässt, löst Send eine Exception aus, und LastResponse enthält weiterhin die ausgewertete Antwort mit ihrem pok, sodass ein bereits erfasster Verkauf nie versehentlich zweimal gesendet wird.

Die Obergrenze von 12 kB

Der Dienst lehnt eine Nachricht, die größer als 12 kB ist, mit dem Fehlercode 7 ab, und BuildMessage prüft die Größe, bevor irgendetwas gesendet wird. Jedes Feld eines Verkaufs ist durch das Schema begrenzt, daher ist der einzige Teil des Envelopes, dessen Größe wirklich variiert, das Signaturzertifikat im wsse:BinarySecurityToken. Das ist auch der Grund, warum der Envelope genau einen SOAP-Header enthält und warum die Komponente keine Möglichkeit bietet, einen weiteren hinzuzufügen.

C++Builder, .NET, der Server und die Befehlszeile

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

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

Probier es aus

Die Delphi-Demo in Demos\Delphi\EET führt den kompletten Ablauf gegen den Playground auf einem einzigen Formular vor. Lade ein Testzertifikat, sende im Verifizierungsmodus, entferne dann das Häkchen und sende einen echten Verkauf, um eine Bestätigung mit ihrem pok zu erhalten. Build Message (no send) zeigt den signierten Envelope, den eine Offline-Warteschlange speichern würde, und Resend Stored Sale wiederholt den letzten Verkauf als erneute Übermittlung. Die Testzertifikate sind nicht in der Demo enthalten, weil das Dokument, mit dem sie ausgegeben werden, nicht öffentlich zugänglich ist. Lade sie daher von eet.gov.cz herunter.

Alle Eigenschaften, Methoden und Ereignisse findest du in der sgcSign Online-Hilfe, und der Abschnitt zu EET 2.0 auf der Seite der sgcSign-Länderprofile fasst die Komponente zusammen.

Verfügbarkeit

TsgcEETClient ist in sgcSign 2026.10 für Delphi, C++Builder und .NET enthalten, zusammen mit der sgcSign Server-Route und dem Verb eet des Befehlszeilenwerkzeugs.

Fragen, oder eine Kasse, die bis Januar fertig sein muss? Nimm Kontakt auf. Wenn sich etwas nicht so verhält, wie du es erwartest, sende das Request- und Response-XML zusammen mit der X-Global-Transaction-Id, und du erhältst eine Antwort von den Leuten, die den Code geschrieben haben.