0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

見積CSVの消費税が1円ずれる理由をPythonで再現する

0
Posted at

見積の合計を見比べて、1円だけ違う。そんなときに「どちらかが間違い」と決めるのは早いです。明細ごとに税額を丸めるか、税抜合計に対して最後に丸めるか。たった3行の見積でも、計算する順番で1円変わります。

今日は手元で見積CSVを3行作り、両方の計算をPythonで出しました。営業側が先に確認したいのは、コードの正しさより相手と自社の計算単位です。そこが揃わないまま数字だけ直すと、次の見積でも同じやり取りになります。

3行で税額が1円違った

試すデータは架空の見積です。単価は円、数量は個数。税率は計算例として10%に固定しました。軽減税率、値引き、送料は入れていません。

sample.csv をUTF-8で保存します。

item,unit_price,qty
初期設定,1005,1
操作説明,2015,1
運用資料,3025,1

税抜の明細金額は1,005円、2,015円、3,025円。合計6,045円です。各明細の税額を四捨五入すると101円、202円、303円で、税額は606円になります。一方、6,045円の10%を最後に四捨五入すると605円。

税込合計はそれぞれ6,651円と6,650円です。

最初は自分の足し算を疑いました。電卓でも6,045×0.1は604.5。税額605円のほうだけを見ていると、明細側の606円が妙に見えます。明細の100.5円、201.5円、302.5円をそれぞれ丸めた結果を並べて、ようやく差の場所がはっきりしました。ここ地味に効きます。

丸める場所を分けて表示する

以下をquote_tax.pyとして保存します。Pythonの標準ライブラリだけで動きます。CSVは円単位の整数だけを受け付け、空のCSVや0以下の数量はエラーにします。

import csv
import sys
from decimal import Decimal, ROUND_HALF_UP

RATE = Decimal("0.10")

def tax(amount: int) -> int:
    raw = Decimal(amount) * RATE
    return int(raw.quantize(Decimal("1"), rounding=ROUND_HALF_UP))

def read_lines(path: str) -> list[tuple[str, int]]:
    with open(path, newline="", encoding="utf-8-sig") as file:
        reader = csv.DictReader(file)
        if reader.fieldnames != ["item", "unit_price", "qty"]:
            raise ValueError("列は item,unit_price,qty の順にしてください")

        lines = []
        for row in reader:
            name = row["item"].strip()
            unit_price = int(row["unit_price"])
            qty = int(row["qty"])
            if not name or unit_price < 0 or qty <= 0:
                raise ValueError("品目・単価・数量を確認してください")
            lines.append((name, unit_price * qty))

    if not lines:
        raise ValueError("明細がありません")
    return lines

lines = read_lines(sys.argv[1])
subtotal = sum(amount for _, amount in lines)
tax_by_line = sum(tax(amount) for _, amount in lines)
tax_by_total = tax(subtotal)

for name, amount in lines:
    print(f"{name}: 税抜{amount:,}円 / 明細の税額{tax(amount):,}円")
print(f"税抜合計: {subtotal:,}円")
print(f"明細ごとに丸める: 税額{tax_by_line:,}円 / 税込{subtotal + tax_by_line:,}円")
print(f"合計後に丸める: 税額{tax_by_total:,}円 / 税込{subtotal + tax_by_total:,}円")
print(f"税込の差: {tax_by_line - tax_by_total:+,}円")

実行はpython3 quote_tax.py sample.csv。手元では次の結果になりました。

初期設定: 税抜1,005円 / 明細の税額101円
操作説明: 税抜2,015円 / 明細の税額202円
運用資料: 税抜3,025円 / 明細の税額303円
税抜合計: 6,045円
明細ごとに丸める: 税額606円 / 税込6,651円
合計後に丸める: 税額605円 / 税込6,650円
税込の差: +1円

端数処理にはDecimalのROUND_HALF_UPを指定しています。round()に任せると、Pythonではちょうど0.5のときに偶数側へ寄せます。今回の100.5円をround(100.5)で計算すると100円。見積で想定した四捨五入とは別の結果です。浮動小数点の小さな誤差も絡めたくないので、税率は文字列からDecimal("0.10")を作りました。

CSVをutf-8-sigで読むのは、ExcelからUTF-8 CSVを書き出したときのBOMが先頭列名に付く場合があるためです。そこを通常のutf-8で読むと、最初の列名が見た目はitemでも実際は別の文字列になります。今回は列名を厳密に見ているので、入口で吸収しておきました。

1円を直す前に、計算単位を聞く

差を見つけたら、相手の見積書で税額が明細ごとに出ているか、税抜合計の下に1つだけ出ているかを見ます。表示だけでは計算方法を断定できない場合もあります。その場合は「税額は明細単位と合計単位のどちらで端数処理していますか」と聞くのが早いです。自社の請求システムにも同じ確認をします。

「1円くらい合わせて送ろう」と手入力する案もあります。僕なら先に止めます。CSVの元金額が変わらず、見積書だけ手で1円動かすと、再発行時に元へ戻るからです。今回のコードは採用すべき方式を決めるものではありません。双方が使っている方式と差額を、同じ入力で並べて話すためのものです。

実務へ持ち込むなら、単価が小数になる契約、値引き行、税率が混在する見積は別途ルールが要ります。このサンプルは円単位の単価と単一税率だけ。品目名にカンマが入るならCSVの引用符も必要です。そこを広げる前に、まずは普段使う見積の税額がどの単位で丸められるかを1件確かめるのがいいです。

3行のサンプルでは、明細ごとなら6,651円、合計後なら6,650円でした。1円差が出たときは税抜合計を見直すだけで終わらせず、丸める位置まで確認する。営業と経理でこの1行を共有しておくと、見積を出し直す前の会話が短くなります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?