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:
- Eine Nachricht pro Verkauf, ein Element
Trzbaim Namespacehttp://fs.gov.cz/eet/schema/v4, per HTTPS mit TLS 1.2 oder höher gesendet. - Eine WS-Security-Signatur über den SOAP-Body, mit exklusiver Kanonisierung, SHA-256 und RSA, wobei das Zertifikat des Steuerpflichtigen in einem
wsse:BinarySecurityTokenmitgeführt wird. - Das Zertifikat des Steuerpflichtigen ist eine PKCS#12-Datei, und sein Common Name ist die Kennung des Steuerpflichtigen.
- Der gesamte Envelope darf 12 kB nicht überschreiten, also 12.288 Byte.
- Die Antwort enthält den pok, den Empfangszeitpunkt und etwaige Warnungen, oder einen Fehlercode.
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:
- 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. - Erstellt das Element
Trzbaund verpackt es in einen SOAP 1.1-Envelope, mit einer neuenuuid_zpravyfür die Nachricht. - 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.
- Misst den fertigen Envelope an der Obergrenze von 12 kB, bevor irgendetwas gesendet wird.
- 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.
- Wertet die Antwort aus: den pok, den Empfangszeitpunkt, das Test-Flag, die Warnungen und den Fehlercode.
- 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:
- Rufe
LoadFromFileauf. Das Setzen vonFileNameundPasswordspeichert nur die Werte, undBuildMessageweigert sich, ohne Zertifikat eine Nachricht zu erstellen. TaxpayerEICmuss dem Common Name des Signaturzertifikats entsprechen, sonst enthält die Antwort Warnung 1. Das Auslesen vonCertificate.SubjectCNgarantiert das.- Eine tatsächlich zugewiesene Nummer der Betriebsstätte hat mindestens zwei Ziffern und endet auf 1, 2, 3 oder 4. Ein ausgedachter Wert wie 1 besteht die Validierung, bringt aber Warnung 6 ein.
- Lies einen von der Kassenkraft eingegebenen Betrag mit
sgcEETParseAmountein, nicht mitStrToCurr, das dem Dezimaltrennzeichen des Rechners folgt.
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:
| Ergebnis | Bedeutung |
|---|---|
eoAcknowledged | Der Verkauf ist gemeldet, und der pok steht in TsgcEETResponse.POK. Dies ist das einzige Ergebnis, das einen Verkauf meldet. |
eoVerified | Der Erfolg im Verifizierungsmodus. Es wurde nichts gemeldet. |
eoRejected | Alles 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:
BuildMessagevalidiert, erstellt, signiert und misst den Envelope, ohne ihn zu senden. Die dabei verwendeteuuid_zpravysteht inLastMessageUUID.SendRawsendet einen gespeicherten Envelope unverändert, einschließlich seiner ursprünglichenuuid_zpravyundprvni_zaslani, was für eine Nachricht richtig ist, die den Rechner nie verlassen hat.Resendwiederholt einen Verkauf, der gesendet, aber nie bestätigt wurde, mit neueruuid_zpravyund mitprvni_zaslaniauf false, was genau der Wiederholung entspricht, die die Spezifikation beschreibt.
// 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
- C++Builder verwendet dieselbe Komponente, und
Demos\CBuilder\EETspiegelt die Delphi-Demo. - .NET hat
TsgcEETClientmit derselben API und eine WinForms-Demo indemos\EET. - sgcSign Server ergänzt
POST /api/v1/sign/eet, das einen Verkauf mit einem auf dem Server hinterlegten Schlüssel erstellt und signiert und ihn auch einreichen und die Antwort zurückgeben kann. Eine Kette von Kassen kann sich dann ein Zertifikat des Steuerpflichtigen teilen, das auf dem Server liegt, statt dass jede Kasse eine Kopie besitzt. - Das Befehlszeilenwerkzeug
sgcsignhat das passende Verbeet. Es liest den Verkauf aus einer JSON-Datei, und sein Exit-Code sagt einem Kassenskript, was passiert ist: 6 bedeutet, dass die Steuerbehörde den Verkauf abgelehnt hat, 5 bedeutet, dass der Server ihn nicht signieren oder die Steuerbehörde nicht erreichen konnte, und 4 bedeutet, dass der sgcSign Server selbst nicht erreichbar war.
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.
