2025年7月31日·1分で読めます

税と請求に対応する複数通貨の価格データモデル

為替レート、丸め、税、ローカライズ表示を扱う複数通貨の価格データモデルを学び、請求書で驚きが起きない仕組みを作りましょう。

税と請求に対応する複数通貨の価格データモデル

複数通貨請求書でよく失敗する点

複数通貨の請求書は地味で高くつく失敗をします。UIでは数字が正しく見えても、PDFを出力して経理が取り込むと、合計が明細の合計と合わなくなることがあります。

根本原因はシンプルです: 金額の計算は単に為替レートを掛けるだけではありません。税、丸め、レートを取得した正確なタイミングが結果に影響します。価格データモデルがこれらの選択を明示していないと、システムの異なる部分が「親切に」再計算して別々の答えを出してしまいます。

次の3つのビューが合意している必要があります。たとえ表示通貨が違っても:

  • 顧客ビュー: 顧客通貨でわかりやすい価格、合計が合っていること。
  • 会計ビュー: レポートや照合のための一貫したベース金額。
  • 監査ビュー: どのレートと丸めルールで請求書が作られたかを示す紙の跡。

不一致は通常、異なる場所で行われた小さな判断から生じます。あるチームは各行で丸める、別のチームは合計だけ丸める。あるページは現在のレートを使い、別のページは請求日レートを使う。割引の前に課税する税もあれば後に課税する税もあります。価格に税が含まれているケースと、上乗せされるケースもあります。

具体例: 19.99 EUR のアイテムを販売し、請求は GBP、報告は USD で行うとします。行ごとに変換して小数第2位で丸めると、先に合計してから一度変換した場合と税合計が異なることがあります。どちらの方法も理にかなっている場合がありますが、規則は一つにしておく必要があります。

目標は計算の予測可能性と保存される値の明確さです。各請求書は推測なしに答えられる必要があります: どの金額が入力され、どの通貨で入力され、どのレートが使われ(いつ)、何がどのように丸められ、どの税ルールが適用されたか。これが明確なら、UI・PDF・エクスポート・監査間で合計が安定します。

スキーマ設計の前に合意すべき重要な用語

テーブルを描く前に、皆が同じ言葉を使っていることを確認してください。多くの複数通貨バグは技術的ではなく「言葉の意味が違っていた」バグです。きれいなスキーマはプロダクト、ファイナンス、エンジニアリングが受け入れる定義から始まります。

データベースに影響する通貨用語

各マネーフローについて、3つの通貨に合意してください:

  • 取引通貨(Transactional currency): 顧客が見る通貨で、価格表・カート・請求書表示に使われる通貨。
  • 決済通貨(Settlement currency): 実際に支払われる通貨(支払いプロバイダや銀行が精算する通貨)。
  • 報告通貨(Reporting currency): ダッシュボードや会計集計に使われる通貨。

また 最小単位(minor units) を定義してください。USDは2(セント)、JPYは0、KWDは3などです。これは重要で、"12.34" を浮動小数点で保存すると誤差が出やすいですが、最小単位の整数(例えば 1234 セント)で保存すると正確さが保たれ、丸めも予測可能になります。

合計を変える税の用語

税も同じレベルで合意を要します。価格が 税込み(tax-inclusive) なのか 税別(tax-exclusive) なのかを決めてください。また税を 行ごとに計算する(per line) のか 請求書合計で計算する(per invoice) のかを選びます。これらの選択は丸めに影響し、最終支払額が数最小単位分変わることがあります。

最後に、何を保存し何を導出可能にするかを決めてください:

  • 保存するもの: 法的・会計的に重要なもの(合意された価格、適用された税率、最終の丸め済み合計、使用した通貨)。
  • 導出可能なもの: 安全に再計算できるもの(フォーマット済みの文字列、表示用の変換、ほとんどの中間計算)。

コアの金額フィールド: 何を保存しどう保存するか

事実として保存する数値と、再計算できる結果を区別することから始めます。これらを混ぜると、画面上の合計とエクスポートの合計が違うといったことが起きます。

金額は最小単位の整数(セントなど)で保存し、常に通貨コードも隣に保存してください。通貨のない金額は不完全なデータです。整数は多数の行を加算しても小さな浮動誤差が出ない利点があります。

実務的なパターンとしては、生データ(ユーザが入力した値)と計算結果の両方を保持します。入力はユーザが何を入力したかを説明し、出力は請求した内容を説明します。数か月後に請求書が争われた場合、両方が必要です。

請求書の行項目に対して、耐久性のあるきれいなフィールド群の例:

  • unit_price_minor + unit_currency
  • quantity(必要なら uom
  • line_subtotal_minor(税・割引前)
  • line_discount_minor
  • line_tax_minor(税種ごとに分けてもよい)
  • line_total_minor(行の最終金額)

丸めは単なるUIの細部ではありません。計算に使った丸め方法と精度を永続化してください。特に JPY と USD のように最小単位が異なる通貨や、現金丸めのルールをサポートする場合は重要です。小さな「計算コンテキスト」レコードで calc_precisionrounding_mode、丸めを行うタイミング(行ごとか合計のみか)を記録できます。

表示フォーマットは保存値と分離してください。保存値はプレーンな数値とコードで、通貨記号・区切り・ローカライズされた表示はプレゼンテーション層の責務です。例えば 12345 + EUR を保存し、UIが "€123.45" や "123,45 €" と表示するかを決めます。

為替レート: テーブル、タイムスタンプ、監査トレイル

為替レートは時系列データであり、明確なソースを持たせて扱ってください。"今日のレート" を後で安全に再計算することはできません。

実用的な為替レートテーブルには通常次が含まれます:

  • base_currency(変換元、例: USD)
  • quote_currency(変換先、例: EUR)
  • rate(1 base あたりの quote、精度の高い小数で保存)
  • effective_at(そのレートが有効なタイムスタンプ)
  • source(提供元)と source_ref(提供元のIDやペイロードハッシュ)

監査ではこのソース情報が重要です。顧客が金額を争えば、どこからその数値が来たかを正確に示せます。

次に、請求書がどのレートを使うかのルールを一つ決め、それを守ってください。一般的な選択肢は注文時・出荷時・請求書発行時のレートです。どれが最適かはビジネス次第ですが、一貫性とドキュメント化が重要です。

いずれのルールを選んでも、請求書に使用した正確なレートを保存してください(できれば各請求行にも)。後で参照して再現できるように、fx_ratefx_rate_effective_atfx_rate_source のようなフィールドを追加します。

週末や祝日、プロバイダ障害でレートがない場合のフォールバック動作も明確にしてください。典型的なアプローチは: 直近の過去のレートを使う、レートが得られるまで請求を止める、手動レートを許可して承認フラグを付ける、などです。

例: 注文が土曜、出荷が月曜、請求が月曜で、ルールが請求時レートだとします。プロバイダが週末のレートを公開しない場合、金曜の最終レートを使い effective_at = Friday 23:59 として source_ref を記録するようにできます。

一貫して維持する通貨変換と丸めルール

為替レートを正しく固定する
レートのタイムスタンプとソースを保存し、アプリ内のあらゆる場所で再利用します。
始める

丸めの問題は明白なバグの形を取りにくいです。請求書合計と行合計の間に1セントの差が出たり、表示と支払プロバイダの期待値で税に小さな差が出たりします。良いモデルは丸めを説明できるルールにします。

丸めをどこで行うかを正確に決める

丸めを許すポイントを選び、それ以外は高精度のままにしてください。一般的な丸めポイント:

  • 行の延長(数量 x 単価、割引適用後)
  • 各税額(行ごとまたは請求書合計で、法域による)
  • 最終請求書合計

これらを定義していないと、システムの別々の部分が都合のいいときに丸めを行い、合計がずれていきます。

1つの丸めモードを使い、税ルールに対する明確な例外を設ける

丸めモード(round half-up や bankers rounding)を選び、一貫して適用してください。half-up は顧客に説明しやすく、bankers rounding は大量取引での偏りを軽減します。どちらでもよいですが、API・UI・エクスポート・会計レポートは同じモードを使う必要があります。

変換や中間ステップでは追加の精度を保ち(FXレートは多数の小数で保存するなど)、選んだ丸めポイントでのみ丸めを行ってください。

割引も単一のルールを必要とします: 割引を税の前に適用するか(クーポンに一般的)、税の後に適用するか(特定手数料で要求される場合あり)。これを文書化して一度だけエンコードしてください。

一部の法域では税を行ごと・税ごと・請求書合計で丸めることが求められます。コードベースのあちこちに一回限りのケースをハードコーディングする代わりに、国/州/税制ごとの rounding policy 設定を保存し、そのポリシーに従って計算するようにしてください。

簡単なチェック: 同じ請求書を翌日に同じ保存済みレートとポリシーで再構築すると、セント単位まで完全に同じ結果になるはずです。

税フィールド: VAT、売上税、多重税のパターン

FXと税の設定を管理する
レート、税ポリシー、承認の管理画面を作り、請求書の安定性を保ちます。
今すぐ始める

税は購入者の場所、商品の種類、価格が税込みか税別かで急速に複雑になります。きれいなモデルは税を暗黙にしないで明示します。

課税の基礎を明確にしてください。課税対象価格がネット(税別)なのかグロス(税込み)なのかを保存し、適用した税率と計算された税額もスナップショットとして保存します。これにより将来的なルール変更で過去の請求書が書き換えられません。

各請求行に最低限保存しておくと良い項目:

  • tax_basis(NET または GROSS)
  • tax_rate(小数、例: 0.20)
  • taxable_amount_minor(実際に課税した基礎)
  • tax_amount_minor
  • tax_method(PER_LINE または ON_SUBTOTAL)

複数税が適用可能な場合(例: VAT と市の付加税)、InvoiceLineTax のような内訳テーブルを作り、適用された税ごとに1行ずつ持たせます。各行には tax_codetax_ratetaxable_amount_minortax_amount_minor、通貨、計算時に使った法域のヒント(国・地域・郵便番号など)を含めます。

請求書や請求行に適用したルールのスナップショット(rule_version や意思決定の入力を含むJSONなど)を保存してください。もし翌年にVATルールが変わっても、古い請求書は実際に請求した内容と一致するべきです。

例: ドイツの顧客に対するSaaSサブスクリプションは NET の行価格に 19% の VAT を適用し、さらに 1% のローカル税を追加するとします。請求行の合計を請求どおり保存し、表示と監査のために税ごとに内訳行を保持します。

テーブル設計の段階的手順

これは巧妙な数学というよりは、正しい事実を正しいタイミングで固定化することです。目標は、数か月後に請求書を再度開いても同じ数字を表示できることです。

まず、プロダクト価格の真実がどこにあるかを決めます。多くのチームは製品ごとに基準通貨の価格を持ち、市場ごとのオーバーライド(USD と EUR の別行など)をオプションで持ちます。何を選ぶにしても、カタログ価格と変換済み価格を混同しないようスキーマで明示してください。

わかりやすくテーブルを保つための単純な流れ:

  • 製品と価格: product_idprice_amount_minorprice_currencyeffective_from(価格変更がある場合)。
  • 注文・請求ヘッダ: document_currencycustomer_localebilling_country、タイムスタンプ(issued_attax_point_at)。
  • 行項目: unit_price_amount_minorquantitydiscount_amount_minortax_amount_minorline_total_amount_minor、および各金額フィールドの通貨。
  • 為替レートスナップショット: 使った正確なレート(rate_valuerate_providerrate_timestamp)を注文または請求書から参照。
  • 税内訳レコード: 各税ごとに1行(tax_typerate_percenttaxable_base_minortax_amount_minor)と calculation_method フラグ。

後で再計算に頼らないでください。請求書を作るときは、最終単価・割引・合計を注文からコピーして請求行に保存してください。

トレーサビリティのために請求書に calculation_version(または calc_hash)を追加し、誰がいつ再計算をトリガーしたかを記録する小さな calculation_log テーブルを用意すると良いです(例: "rate updated before issuing")。

表示をローカライズしても数字を壊さない方法

請求書スキーマを設計する
請求書、税、FXテーブルを1か所でモデリングします。
AppMasterを試す

ローカライズは請求書の見た目を変えるべきで、意味を変えるべきではありません。計算は保存された数値(最小単位の整数や固定精度小数)を使って行い、最後にロケールごとのフォーマットを適用してください。

請求書の表示設定は顧客プロファイルだけでなく請求書自体にも保持してください。顧客は国や請求先を変えることがあるため、請求書は法的なスナップショットです。invoice_languageinvoice_locale、小数表示の有無などのフォーマットフラグをドキュメントに保存しておけば、6か月後の再印刷でも元と一致します。

通貨記号は表示上の問題です。あるロケールは記号を金額の前に置き、別のロケールは後に置きます。スペースの有無、小数区切り、千位区切りもレンダリング時に処理し、保存値に記号を混入させないでください。フォーマット済み文字列を再び数値にパースするのも避けてください。

二次通貨でのレポートが必要なら(多くは本国通貨: USD や EUR)、それは二次合計として明示的に表示してください。ドキュメント通貨が法的な真実です。

実用的な請求書出力の設定:

  • 行項目と合計はドキュメント通貨で表示し、請求書ロケールに従ってフォーマットする。
  • 必要ならソースとタイムスタンプを明記した二次報告通貨の合計をオプションで表示する。
  • 税の内訳は(課税基礎、各税、税合計)として別行で表示し、単一の混合額にしない。
  • PDF とメールは同じ保存済み合計からレンダリングし、数字がずれないようにする。

例: フランスの顧客に CHF で請求する場合、請求書ロケールは小数にコンマを使い通貨記号を金額の後に置くが、計算は保存済みの CHF 金額と税合計を使う。表示だけが変わり、数値は変わりません。

避けるべき一般的なミスと落とし穴

複数通貨請求を壊す最速の方法は、お金を普通の数値として扱うことです。価格・税・合計に浮動小数点型を使うと小さな誤差が生じ、後で "0.01 のズレ" が出ます。金額は最小単位の整数(セント)か明確なスケールを持つ固定小数型で保存し、一貫して使ってください。

もう一つの古典的な落とし穴は履歴を誤って変更することです。古い請求書を今日の為替レートや更新された税ルールで再計算すると、顧客が見て支払ったドキュメントでなくなります。請求書は不変にしてください: 発行後は使用した正確な為替レート、丸めルール、税方法を保存し、保存済み合計を再計算しないでください。

単一の行で通貨を混在させるのも静かなスキーマバグです。単価が EUR、割引が USD、税が GBP のように混ざると後で計算を説明できません。表示・決済用のドキュメント通貨と、必要なら内部報告用の基準通貨を一つずつ選んでください。保存するすべての金額は通貨を明示するべきです。

丸めのミスは多くの場合、頻繁に丸めすぎることから生じます。単価で丸め、行合計で丸め、行ごとの税で丸め、小計で再度丸めると合計が行の和と合わなくなります。

注意すべき一般的な落とし穴:

  • 金額や為替レートに浮動小数点を使うこと
  • 古い請求書を再計算して今日のレートを使うこと
  • 1つの行項目に複数通貨を混在させること
  • 明確に定義されたポイントではなく多数のステップで丸めること
  • ドキュメントごとにレートタイムスタンプ、丸めモード、税方法を保存していないこと

例: CAD で請求書を作り、EUR 設定のサービスを変換して表示するだけにしておくと、レート表を後で更新したときに CAD 合計が翌週変わってしまいます。EUR 金額、適用した FX レートとその時間、最終 CAD 金額を請求書に保存してください。

出荷前の簡易チェックリスト

手戻りを避ける
要件変更時に乱雑な書き換えをせずに、実際のバックエンドとアプリコードを生成します。
始める

複数通貨請求書を "完了" とする前に一貫性に焦点を当てた最終確認を行ってください。ここでのほとんどのバグは複雑ではなく、保存しているもの・表示しているもの・合計しているものの不一致から来ます。

リリースゲートとして次を確認してください:

  • 各請求書ヘッダには正確に1つのドキュメント通貨があり、請求書に保存したすべての合計はその通貨であること。
  • 保存するすべての金額は最小単位の整数(行合計・税額・割引・送料を含む)であること。
  • 請求書は使用した正確な為替レート(精密な小数)とタイムスタンプ、レートソースを保存していること。
  • 丸めルールは文書化され、共通の場所で実装されていること。
  • 複数税があり得る場合、行ごと(および必要なら法域ごと)の税内訳を保存していること。

スキーマを確認したら、監査人がやるように計算を検証してください。請求書合計は保存済みの行合計と保存済みの税額の合計と等しくあるべきです。表示された値やフォーマット済み文字列から合計を再計算しないでください。

実用的なテスト: 3行以上ある請求書を1つ選び、割引を適用し、1行に2種類の税を含めて別ロケールで印刷し(区切り記号や通貨記号が異なる)、保存済みの数値が変わらないことを確認してください。

例シナリオ: 1注文、3通貨、税あり

請求のプロトタイピングを高速化する
同じロジックからウェブとモバイルUIを持つ内部課金ツールをすばやく立ち上げます。
プロトタイプを作る

顧客通貨が USD、仕入先は EUR、経理は GBP で報告するケースです。ここでモデルがうまくいくか、1セントの不一致の山になるかが分かれます。

注文: 製品を3個注文。

  • 顧客価格: $19.99 / 単位(USD)
  • 割引: 行に対して 10% の割引
  • 米国売上税: 8.25%(割引後に課税)
  • 仕入れコスト: EUR 12.40 / 単位(EUR)
  • 報告通貨: GBP

起こることと変換のタイミングの説明

変換のタイミングを一つ選んで守ってください。多くの請求システムでは、請求発行時に変換してそのレートを保存するのが安全な選択です。

請求書作成時の手順:

  1. USD の行小計を計算: 3 x 19.99 = 59.97 USD。
  2. 割引を適用: 59.97 x 10% = 5.997 → 6.00 USD に丸め。
  3. 行のネット金額: 59.97 - 6.00 = 53.97 USD。
  4. 税: 53.97 x 8.25% = 4.452525 → 4.45 USD に丸め。
  5. 合計: 53.97 + 4.45 = 58.42 USD。

丸めは定義されたポイント(割引、各税額、行合計)でのみ行い、これらの丸め済み結果を保存してください。保存済みの値を常に合計することで、PDF では 58.42 なのにエクスポートで 58.43 と出るという古典的な問題を防げます。

後で請求書を再現するために保存するもの

請求書(と請求行)には通貨コード(USD)、最小単位での金額(セント)、税の内訳を税種ごとに、そして USD から GBP に変換するために使った為替レートのレコードIDを保存します。仕入れコストについても EUR コストと、それを GBP に変換する場合はそのためのレートレコードを保存します。

顧客は USD の明瞭な請求書(価格、割引、税、合計)を見ます。経理は USD 金額に加えて凍結された GBP 換算値と正確なレートタイムスタンプをエクスポートし、翌月の数字もレートが変わっても一致します。

次のステップ: 実装・テスト・保守性の確保

保存する最小限のスキーマを短い契約として書き出してください: どの金額を保存するか(原本、変換後、税)、各金額の通貨、どの丸めルールを適用するか、請求書にレートを固定するためのタイムスタンプ。退屈で具体的に作ることが重要です。

UI を作る前にテストを作成してください。通常の請求書だけでなく、丸めノイズを露呈するようなエッジケースや集計問題を暴くケースも追加してください。

開始用のテストケース:

  • 小さな単価(0.01 など)に大きな数量を掛けるケース
  • 変換後に循環小数が出るような割引
  • 注文日と請求日で為替レートが変わるケース
  • 同一請求書での混在する税ルール(税込み vs 税別)
  • 元の丸めに合わせる必要がある返金・クレジットノート

サポートチケットを短くするため、請求書上のすべての数値を説明する監査ビューを追加してください: 保存済みの金額、通貨コード、為替レートIDとタイムスタンプ、丸め方式。誰かに "なぜ合計が違うの?" と聞かれたら、保存された事実から答えられるようにします。

内部向けの課金ツールを作るなら、AppMaster(appmaster.io)のようなノーコードプラットフォームはスキーマを一箇所に置き、計算ロジックを再利用可能なワークフローにまとめることで、ウェブとモバイルの画面ごとに個別に計算を実装する手間を減らせます。

最後に、責任の所在を決めてください。誰が為替レートを更新するか、誰が税ルールを更新するか、発行済み請求書に影響する変更を誰が承認するか。安定性はスキーマだけでなくプロセスでも達成されます。

始めやすい
何かを作成する 素晴らしい

無料プランで AppMaster を試してみてください。
準備が整ったら、適切なサブスクリプションを選択できます。

始める