見積の合計を見比べて、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行を共有しておくと、見積を出し直す前の会話が短くなります。