チェコ共和国は売上の電子登録制度を復活させます。EET 2.0(Elektronická evidence tržeb)では、店舗の POS が各売上を発生と同時に税務当局に報告し、税務当局は受領確認コードである pok を返します。pok は売上が報告されたことの証明です。報告は 2027 年 1 月 1 日に開始され、レジを構築してテストするためのプレイグラウンドはすでに公開されています。
sgcSign には、このための新しいコンポーネント TsgcEETClient が加わりました。売上を検証し、メッセージを構築し、納税者の証明書で署名して送信し、受領確認の署名をチェックして結果を返します。本記事では、EET 2.0 が求める内容、コンポーネントが往復処理をどのように行うか、そして最初の売上、オフラインキュー、検証済みの受領確認のための Delphi コードを説明します。
Delphi デモによる売上登録。検証モードから実際の pok の取得までを紹介します。YouTube でもご覧いただけます。
EET 2.0 は更新ではなく新しいプロトコル
最初の EET 制度向けにレジを構築したことがある場合は、白紙の状態から始めてください。データインターフェース バージョン 4.1 は旧バージョン 3.1 と互換性がなく、しかもより単純です。計算すべき PKP や BKP のセキュリティコードはなく、VAT の内訳もなく、TLS クライアント証明書も不要です。売上は 10 個のデータ属性で表されます。残るのは標準的な SOAP 1.1 Web サービスです。
- 売上 1 件につき 1 メッセージで、
Trzba要素(名前空間http://fs.gov.cz/eet/schema/v4)を TLS 1.2 以上の HTTPS で送信します。 - SOAP 本文に対する WS-Security 署名。エクスクルーシブ正規化、SHA-256、RSA を使用し、納税者の証明書を
wsse:BinarySecurityTokenに格納します。 - 納税者の証明書は PKCS#12 ファイルで、そのコモンネームが納税者識別子です。
- エンベロープ全体は 12 kB、つまり 12,288 バイトを超えてはなりません。
- 応答には pok、受信日時、警告が含まれるか、またはエラーコードが含まれます。
納税者としての登録、MOJE daně ポータルでの証明書の取得、事業所番号の割り当ては、すべてコードを実行する前に済ませておく手続きです。これらの手続きの結果のうち、ライブラリが必要とするのは PKCS#12 ファイル 1 つと、納税者識別子および事業所識別子という 2 つの番号です。
TsgcEETClient による往復処理
Send を 1 回呼び出すだけで、すべてのステップが次の順序で実行されます。
TsgcEETSaleレコードのすべてのフィールドをスキーマの規則に照らして検証します。無効な売上は理由がわかる形でローカルで拒否され、サービスに届くことはありません。Trzba要素を構築し、メッセージ用の新しいuuid_zpravyを付けて SOAP 1.1 エンベロープで包みます。- 任意の sgcSign キープロバイダーの鍵を使い、WS-Security で SOAP 本文に署名します。PFX ファイル、Windows 証明書ストア、PKCS#11 トークンやスマートカード、クラウドの鍵サービスを利用できます。
- 何かを送信する前に、完成したエンベロープのサイズを 12 kB の上限と照らし合わせて計測します。
- 税務当局に送信します。デフォルトのエンドポイントはプレイグラウンドなので、フォームに配置しただけのコンポーネントが誤って実際の売上を登録することはありません。
- 応答を解析し、pok、受信日時、テストフラグ、警告、エラーコードを取り出します。
- 受領確認の署名を検証します。エラー応答は設計上署名されていないため、拒否が署名エラーに変わることはありません。
Delphi での最初の売上
仕様では、納税者はまず検証モードから始めるよう求められています。メッセージは実際のものとまったく同じように完全にチェックされた後に破棄されるため、何も登録されません。これに合格すれば、証明書、署名、TLS 接続、売上のすべてのフィールドが正しいことになります。以下のコードは、まずこのチェックを行い、その後に売上を実際に登録します。
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;
最初の実行がうまくいくかどうかは、いくつかの細かな点で決まります。
LoadFromFileを呼び出してください。FileNameとPasswordを設定しても値が記録されるだけで、BuildMessageは証明書のないメッセージの構築を拒否します。TaxpayerEICは署名証明書のコモンネームと一致している必要があります。一致しない場合、応答に警告 1 が付きます。Certificate.SubjectCNを読み取れば確実に一致します。- 実際に割り当てられた事業所番号は 2 桁以上で、末尾は 1、2、3、4 のいずれかです。1 のような架空の値は検証を通過しますが、警告 6 が返されます。
- レジ担当者が入力した金額は
sgcEETParseAmountで読み取ってください。StrToCurrはマシンの小数点記号に従うため、使用しないでください。
プレイグラウンド向けには、税務行政機関が CZ00000019 などの共有テスト証明書を eet.gov.cz で公開しています。プレイグラウンドからの受領確認には test="true" が含まれ、pok の末尾は ff になります。これは実際の売上について何も証明しません。
応答の読み取り
プロトコルの中で最も意外に感じやすいのがこの部分です。拒否を含むすべての結果が HTTP 200 で届くため、HTTP ステータスからは何もわかりません。さらに、検証モードの成功はコード 0 を持つエラー要素の中で届くため、まったく問題のない検証実行でも TsgcEETResponse.IsError は True になります。
sgcEETResponseOutcome は両方の規則を適用し、次の 3 つのうちいずれかを返します。
| 結果 | 意味 |
|---|---|
eoAcknowledged | 売上は報告済みで、pok は TsgcEETResponse.POK に入っています。売上を報告する結果はこれだけです。 |
eoVerified | 検証モードの成功です。何も登録されていません。 |
eoRejected | それ以外のすべてです。売上は報告されておらず、税務当局への報告義務が引き続き残っています。 |
警告は致命的ではありません。有効な受領確認に最大 10 件の警告が付くことがあり、OnWarning は警告ごとに 1 回発生します。一方、OnError はコードが 0 以外のエラーで発生します。すべてのメッセージの後に LastTransactionId、つまり X-Global-Transaction-Id レスポンスヘッダーをログに記録してください。EET サポートが最初に求めるのがこの値です。LastRequestXML と LastResponseXML は、両方のメッセージを送受信されたとおりに保持します。
回線が切断されたとき: オフラインキュー
レジは接続が切れても販売を続けなければなりません。TsgcEETClient は往復処理を分割しているため、POS はメッセージをキューに入れて後で送信できます。
BuildMessageは、エンベロープを送信せずに検証、構築、署名、サイズの計測を行います。使用されたuuid_zpravyはLastMessageUUIDに入ります。SendRawは、保存済みのエンベロープを、元のuuid_zpravyとprvni_zaslaniを含めてそのまま送信します。一度もマシンから送り出されていないメッセージにはこれが適切です。Resendは、送信済みで受領確認のない売上を、新しいuuid_zpravyと false に設定したprvni_zaslaniで再送します。これが仕様に記載されている再試行です。
// 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);
キューを構築する前に知っておくべき落とし穴が 1 つあります。売上日時はタイムゾーンのオフセット付きで書き込まれ、売上レコードが独自のオフセットを持っていない場合、ライブラリはメッセージを構築した時点のマシンのオフセットを使用します。7 月の売上を 12 月に Resend で再送すると、12 月のオフセットが付けられてしまいます。オフセットは売上と一緒に保存し、再送する際に SaleOffsetMinutes と HasSaleOffsetMinutes を設定してください。
受領確認の検証
VerifyResponseSignature はデフォルトで True なので、すべての受領確認の署名は初期状態のままでチェックされます。証明書チェーンもチェックするには適切なトラストアンカーが必要ですが、それは一見して思い浮かぶものではありません。受領確認は、プレイグラウンドのテスト資料に付属する EET 証明書ではなく、商用の I.CA 証明書で署名されており、I.CA のどちらの発行者も Windows のルートストアには含まれていません。I.CA Root CA/RSA 05/2022 と I.CA Public CA/RSA 06/2022 を ica.cz からダウンロードし、アンカーとして指定してください。
oClient.TrustedCertificates.Add('ica-root-ca-rsa-05-2022.cer');
oClient.TrustedCertificates.Add('ica-public-ca-rsa-06-2022.cer');
oClient.RequireTrustedChain := True;
チェックに失敗した場合は、LastVerificationDetails が失敗したステップを報告します。受領確認は届いたもののその署名が検証できない場合、Send は例外を発生させますが、LastResponse には pok を含む解析済みの応答が残っているため、すでに登録された売上を誤って 2 回送信することはありません。
12 kB の上限
サービスは 12 kB を超えるメッセージをエラーコード 7 で拒否し、BuildMessage は何かを送信する前にサイズをチェックします。売上のフィールドはすべてスキーマで上限が決められているため、エンベロープの中でサイズが実際に変わるのは wsse:BinarySecurityToken に入る署名証明書だけです。エンベロープに SOAP ヘッダーがちょうど 1 つしかなく、コンポーネントに別のヘッダーを追加する手段がないのもこのためです。
C++Builder、.NET、サーバー、コマンドライン
- C++Builder では同じコンポーネントを使用し、
Demos\CBuilder\EETは Delphi デモと同じ構成です。 - .NET には同じ API を持つ
TsgcEETClientと、demos\EETの WinForms デモがあります。 - sgcSign Server には
POST /api/v1/sign/eetが追加されました。サーバーが保持する鍵で売上を構築して署名し、さらに送信して応答を返すこともできます。これにより、複数店舗のレジは、各レジがコピーを持つ代わりに、サーバー上に保管された 1 つの納税者の証明書を共有できます。 sgcsignコマンドラインツールには、対応するeet動詞があります。売上を JSON ファイルから読み取り、終了コードでレジのスクリプトに結果を伝えます。6 は税務当局が売上を拒否したこと、5 はサーバーが署名できなかったか税務当局に到達できなかったこと、4 は sgcSign サーバー自体に到達できなかったことを意味します。
set SGCSIGN_SERVER=https://sign.shop.local:8443
set SGCSIGN_APIKEY=sgcsk_...
sgcsign eet --provider eet-taxpayer --submit sale.json
試してみる
Demos\Delphi\EET の Delphi デモでは、1 つのフォーム上でプレイグラウンドに対する往復処理全体を試せます。テスト証明書を読み込んで検証モードで送信し、その後チェックを外して実際の売上を送信すると、pok 付きの受領確認が得られます。Build Message (no send) はオフラインキューが保存する署名済みエンベロープを表示し、Resend Stored Sale は最後の売上を再送信として送り直します。テスト証明書を配布する文書は公開が制限されているため、テスト証明書はデモに含まれていません。eet.gov.cz からダウンロードしてください。
すべてのプロパティ、メソッド、イベントは sgcSign オンラインヘルプに記載されており、sgcSign 国別プロファイルページの EET 2.0 セクションにはコンポーネントの概要がまとめられています。
提供状況
TsgcEETClient は Delphi、C++Builder、.NET 向けの sgcSign 2026.10 に含まれ、sgcSign Server のルートとコマンドラインツールの eet 動詞も同時に提供されます。
ご質問がある場合や、1 月までに準備を整えなければならないレジがある場合は、お問い合わせください。期待どおりに動作しない場合は、リクエストとレスポンスの XML を X-Global-Transaction-Id と一緒にお送りください。コードを書いた本人から返信が届きます。
