婚礼・宴会の見積書の金額計算を、見積もりシステムに実装する。サービス料の掛け方、税率ごとに1回の端数処理、値引きの按分、2027年4月からの飲食料品の税率引下げ(法案の成立が前提)に備えて施行日で税率を決める作り
婚礼や
この
この
株式会社bundlyzeでは、
見積もりの金額は、5つの段階の順番を決めてから計算する
見積書の
| 順番 | 段階 | この |
|---|---|---|
| 1 | 数量 | 品目の |
| 2 | サービス料 | 対象の |
| 3 | 税率ごとの |
品目の |
| 4 | 値引き | 税率ごとの |
| 5 | 消費税 | 値引き後の |
サービス料を
税率は、品目の税区分と譲渡の日から表を引いて決める
税率は
婚礼や
税率の
| 税区分 | 適用の期間 | 税率 | 根拠 |
|---|---|---|---|
| standard |
2019-10-01〜 | 10% | 現行 |
| food |
2019-10-01〜2027-03-31 | 8% | 現行の |
| food |
2027-04-01〜2029-03-31 | 1% | 2026年9月15日閣議決定の |
2029年4月1日以後の
2027年4月からの飲食料品の税率引下げ案で、見積もりシステムが確かめること
婚礼の
- 期間と
税率。 2027年4月1日から2029年3月31日までの 2年間、 飲食料品の 譲渡の 税率が 1% (国・地方の 合計)に なる (問1-1) - 対象の
範囲。 今8%の対象に なっている 飲食料品の 範囲と 同じ。 外食や ケータリング等は 対象外で 10%の まま (問1-2) - いつの
取引から。 2027年4月1日以後に行う 飲食料品の 譲渡から。 3月31日までに 結んだ 契約に 基づく ものでも、 4月1日以後に 行う ものは、 経過的な 取扱いに 当たる 場合を 除いて 1% (問1-3) - 経過的な
取扱い。 一定の定期供給契約と 通信販売に 基づく ものは 8%の まま。 選んで 使う ものではない (問1-6) - 前提。 Q&Aの
内容は、 法案が 国会に 提出され、 審議を 経て 可決・成立した 場合の もの
見積もりシステムで
サービス料は、掛ける品目と税区分を設定として持つ
サービス料は、
サービス料
値引きは、税率ごとの金額の比で按分する
税率の
この
消費税の端数処理は、税率ごとに1回だけにする
消費税の
見積書
実装したコード(estimate.mjs)
金額は
// 婚礼・宴会の見積もりの金額計算(依存なし)。金額はすべて円の整数で持つ
// 税区分ごとの税率と、適用する期間。表にない日付は計算を止める(推測で埋めない)
export const RATE_TABLE = [
{ cat: 'standard', from: '2019-10-01', to: null, rate: 10 },
{ cat: 'food', from: '2019-10-01', to: '2027-03-31', rate: 8 },
// 2026-09-15 閣議決定の大綱による引下げ。法案が成立した場合の内容
{ cat: 'food', from: '2027-04-01', to: '2029-03-31', rate: 1 },
];
export function rateFor(cat, date, table = RATE_TABLE) {
const hit = table.find((r) => r.cat === cat && r.from <= date && (r.to === null || date <= r.to));
if (!hit) throw new Error(`税率が決まっていない: ${cat} ${date}`);
return hit.rate;
}
const ROUND = { floor: Math.floor, ceil: Math.ceil, round: Math.round };
// 品目の数量を、人数・卓数・1式から決める
function quantity(item, est) {
if (item.unit === 'guest') return est.guests;
if (item.unit === 'table') return Math.ceil(est.guests / est.perTable);
return item.qty ?? 1;
}
export function calculate(est, table = RATE_TABLE) {
const round = ROUND[est.rounding];
if (!round) throw new Error(`端数処理の方法が不明: ${est.rounding}`);
// 1. 品目の行(税抜)。譲渡の日は品目ごとに持てる(無ければ施行日)
const lines = est.items.map((it) => {
const qty = quantity(it, est);
const date = it.date ?? est.eventDate;
return { ...it, qty, date, amount: it.unitPrice * qty, rate: rateFor(it.cat, date, table) };
});
// 2. サービス料:対象の行の合計に率を掛け、1円未満は端数処理。税区分は設定で決める
const base = lines.filter((l) => l.service).reduce((s, l) => s + l.amount, 0);
if (est.serviceRate > 0) {
lines.push({ name: `サービス料(${est.serviceRate}%)`, cat: est.serviceCat, date: est.eventDate,
amount: round((base * est.serviceRate) / 100), rate: rateFor(est.serviceCat, est.eventDate, table) });
}
// 3. 税率ごとに合計する
const groups = new Map();
for (const l of lines) groups.set(l.rate, (groups.get(l.rate) ?? 0) + l.amount);
// 4. 値引き:税率ごとの合計の比で按分する。端数は最後の税率に寄せ、合計を必ず一致させる
const rates = [...groups.keys()].sort((a, b) => b - a);
const before = rates.reduce((s, r) => s + groups.get(r), 0);
const discount = est.discount ?? 0;
let left = discount;
const byRate = rates.map((rate, i) => {
const d = i === rates.length - 1 ? left : Math.floor((discount * groups.get(rate)) / before);
left -= d;
const net = groups.get(rate) - d;
// 5. 消費税は税率ごとに1回だけ端数処理する
return { rate, gross: groups.get(rate), discount: d, net, tax: round((net * rate) / 100) };
});
const net = byRate.reduce((s, g) => s + g.net, 0);
const tax = byRate.reduce((s, g) => s + g.tax, 0);
return { lines, byRate, net, tax, total: net + tax };
}品目の date は、
施行日を2027年4月1日の前と後にして、同じ見積もりを流した結果
同じ
import { calculate } from './estimate.mjs';
// 単価は計算を確かめるための仮の値(相場ではない)
const items = [
{ name: '料理', unit: 'guest', unitPrice: 10000, cat: 'standard', service: true },
{ name: '飲み物', unit: 'guest', unitPrice: 3333, cat: 'standard', service: true },
{ name: '会場使用料', unit: 'set', unitPrice: 100000, cat: 'standard', service: true },
{ name: '装花(卓ごと)', unit: 'table', unitPrice: 8000, cat: 'standard', service: false },
// 持ち帰りの菓子。ここでは仮に「飲食料品の譲渡」に置く(実際の区分は税理士と決める)
{ name: '持ち帰りの菓子', unit: 'guest', unitPrice: 1111, cat: 'food', service: false },
];
const base = { guests: 61, perTable: 8, items, serviceRate: 10, serviceCat: 'standard', rounding: 'floor', discount: 50000 };
for (const eventDate of ['2027-03-27', '2027-04-10']) {
const r = calculate({ ...base, eventDate });
console.log(`施行日 ${eventDate}`);
for (const l of r.lines) console.log(` ${l.name}:${l.amount} 円(税率 ${l.rate}%)`);
for (const g of r.byRate) console.log(` [${g.rate}%] 合計 ${g.gross} − 値引き ${g.discount} = ${g.net} 消費税 ${g.tax}`);
console.log(` 税抜 ${r.net} + 消費税 ${r.tax} = 税込 ${r.total} 円\n`);
}
try { calculate({ ...base, eventDate: '2029-04-07' }); } catch (e) { console.log(`施行日 2029-04-07 → ${e.message}`); }2026年10月5日に
施行日 2027-03-27
料理:610000 円(税率 10%)
飲み物:203313 円(税率 10%)
会場使用料:100000 円(税率 10%)
装花(卓ごと):64000 円(税率 10%)
持ち帰りの菓子:67771 円(税率 8%)
サービス料(10%):91331 円(税率 10%)
[10%] 合計 1068644 − 値引き 47018 = 1021626 消費税 102162
[8%] 合計 67771 − 値引き 2982 = 64789 消費税 5183
税抜 1086415 + 消費税 107345 = 税込 1193760 円
施行日 2027-04-10
料理:610000 円(税率 10%)
飲み物:203313 円(税率 10%)
会場使用料:100000 円(税率 10%)
装花(卓ごと):64000 円(税率 10%)
持ち帰りの菓子:67771 円(税率 1%)
サービス料(10%):91331 円(税率 10%)
[10%] 合計 1068644 − 値引き 47018 = 1021626 消費税 102162
[1%] 合計 67771 − 値引き 2982 = 64789 消費税 647
税抜 1086415 + 消費税 102809 = 税込 1189224 円
施行日 2029-04-07 → 税率が決まっていない: food 2029-04-07違いが
テストで固定した7つのこと
税率のnode:test)で
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { calculate, rateFor } from './estimate.mjs';
const est = (over = {}) => ({ eventDate: '2027-03-27', guests: 61, perTable: 8, serviceRate: 0,
serviceCat: 'standard', rounding: 'floor', discount: 0, items: [], ...over });
test('税率は譲渡の日で決まる。2027-03-31 と 04-01 の境目', () => {
assert.equal(rateFor('food', '2027-03-31'), 8);
assert.equal(rateFor('food', '2027-04-01'), 1);
assert.equal(rateFor('standard', '2027-04-01'), 10);
});
test('表にない期間は計算を止める', () => {
assert.throws(() => rateFor('food', '2029-04-01'), /税率が決まっていない/);
});
test('品目ごとの日付が施行日より優先される', () => {
const r = calculate(est({ eventDate: '2027-04-03',
items: [{ name: '先に渡す菓子', unit: 'set', unitPrice: 1000, cat: 'food', date: '2027-03-31' }] }));
assert.equal(r.lines[0].rate, 8);
});
test('卓数は人数を1卓の人数で割って切り上げる', () => {
const r = calculate(est({ items: [{ name: '装花', unit: 'table', unitPrice: 1000, cat: 'standard' }] }));
assert.equal(r.lines[0].qty, 8); // 61 ÷ 8 = 7.625 → 8卓
});
test('サービス料は対象の行だけに掛け、1円未満を切り捨てる', () => {
const r = calculate(est({ serviceRate: 10, items: [
{ name: '料理', unit: 'set', unitPrice: 12345, cat: 'standard', service: true },
{ name: '装花', unit: 'set', unitPrice: 50000, cat: 'standard', service: false },
] }));
assert.equal(r.lines.at(-1).amount, 1234); // 12345 × 10% = 1234.5 → 1234
});
test('消費税は税率ごとに1回だけ端数処理する(行ごとに処理した合計とは違う)', () => {
const items = [1, 2, 3].map((i) => ({ name: `菓子${i}`, unit: 'set', unitPrice: 105, cat: 'food' }));
const r = calculate(est({ items }));
const perLine = items.reduce((s, it) => s + Math.floor((it.unitPrice * 8) / 100), 0);
assert.equal(r.tax, 25); // 315 × 8% = 25.2 → 25
assert.equal(perLine, 24); // 8.4 → 8 を3回足すと 24
});
test('値引きは税率ごとに按分し、按分の合計は値引き額と一致する', () => {
const r = calculate(est({ discount: 10001, items: [
{ name: '料理', unit: 'set', unitPrice: 70000, cat: 'standard' },
{ name: '菓子', unit: 'set', unitPrice: 30001, cat: 'food' },
] }));
assert.equal(r.byRate.reduce((s, g) => s + g.discount, 0), 10001);
assert.equal(r.net, 100001 - 10001);
});node --test estimate.test.mjs の
ok 1 - 税率は譲渡の日で決まる。2027-03-31 と 04-01 の境目
ok 2 - 表にない期間は計算を止める
ok 3 - 品目ごとの日付が施行日より優先される
ok 4 - 卓数は人数を1卓の人数で割って切り上げる
ok 5 - サービス料は対象の行だけに掛け、1円未満を切り捨てる
ok 6 - 消費税は税率ごとに1回だけ端数処理する(行ごとに処理した合計とは違う)
ok 7 - 値引きは税率ごとに按分し、按分の合計は値引き額と一致する
# tests 7
# pass 7
# fail 0見積書と、Webサイトのプランや見積り例とで違うこと
消費税の
公式サイトの
作り始める前に、式場・宴会場の側で決めること
計算の
- 品目ごとの
税区分。 持ち帰る品目など、 標準の 区分ではない 可能性が ある 品目を 洗い 出して 確かめる - 譲渡の
日の 施行日で決め方。 一律に するか、 先に 渡す品目に 別の 日を 持たせるか - サービス料の
対象と 値引きの率、 計算の 順番。 前か 後か、 サービス料の 区分 - 値引きの
按分の 金額の方法。 比で 按分するか、 決まった 税率から 引いて 書類に 明記するか - 端数処理の
方法。 切捨て・切上げ・四捨五入のどれに するか (請求書と そろえる) - 税率の
表を 法案の誰が いつ直すか。 成立や 政令の 公布を 見て、 表を 更新する 担当と 手順
株式会社bundlyzeでは、
動作確認した環境
確認日は
- macOS 26.6.2、
Node.js 22.23.2 ( node:test) - 記事の
コード (estimate.mjs・demo.mjs・estimate.test.mjs)を 手元で 流した 出力を、 そのまま 載せています
確かめていない
- 法案の
国会での 成立と、 成立後の 条文・政令の 中身 (この 記事の 表は 2026年9月の 国税庁の Q&Aに 基づく) - 婚礼・宴会で
扱う 個々の 品目 (持ち帰りの 菓子など)の 税区分。 記事では 仮に 置いただけです - 婚礼の
契約や 品目が、 定期供給契約・通信販売の 経過的な 取扱いに 当たるか どうか - サービス料の
扱いの うち、 財務省の 例 (飲食店の サービス料) 以外の 形の もの
参照した公式資料(確認日:2026年10月5日)
税率の
- 2027年4月1日〜2029年3月31日の
飲食料品の 税率1%、 対象の 範囲 (今の 8%と 同じ・ 外食や ケータリング等は 10%)、 4月1日以後の 譲渡から 適用、 経過的な 取扱い (定期供給契約・通信販売)、 法案の 成立が 前提である こと、 飲食料品の 総額表示の 特例 → 国税庁 「飲食料品に 係る 」2年間の 消費税率引下げに 関する Q&A (令和8年9月) 問1-1・1-2・1-3・1-6 - 特設サイトの
開設 (2026年9月15日)と、 大綱が 2026年9月15日の 閣議決定である こと → 国税庁 「消費税率引下げ 特設サイト 」 - ホテルなどの
宴会場での 飲食料品の 提供は 「食事の 提供」で 軽減税率の 対象外 (問72)、 まとめて 値引きした ときの 按分と、 税率ごとの 値引き額が 書類で 確かめられる 場合の 扱い (問93) → 国税庁 「消費税の 軽減税率制度に 」関する Q&A (個別事例編) (令和8年4月改訂) - 適格請求書の
消費税額の 端数処理は 1枚に つき税率ごとに 1回、 方法は 任意、 品目ごとの 端数処理の 合計は 認められない → 国税庁 「適格請求書に 記載する 」消費税額等の 端数処理 (問57) - 見積書・契約書・請求書は
総額表示の 義務の 対象外、 Webの 「見積り例」は 対象、 サービス料を 含む 計算の 例 → 財務省 「総額表示に 関する 」主な 質問に ついて - 総額表示の
義務の 概要 (あらかじめ表示する 価格が 対象、 口頭の 価格は 対象外) → 国税庁 「No.6902 「総額表示」の 」義務付け
よくある質問
見積書の消費税は、品目の行ごとに計算して足してもよいですか?
請求書を
2027年4月から飲食料品の消費税率が1%になると聞きました。婚礼の料理も下がりますか?
国税庁の
今年結んだ契約で、2027年4月以後に行う結婚式の税率はどうなりますか?
国税庁の
値引きは、どの税率の品目から引けばよいですか?
国税庁の