2014 04 02

消費税

昨日から消費税が8%になった。 金を扱うシステムを扱っている人は、きっと先月は大変な思いをしたんだろう。 そして、きっと無事を祈って4月1日を迎えたんだろう。 そんな祈りが届かなかったという話。 TOKYO Web から。

いなげやは31日深夜から1日朝にかけて、本社から各店舗に商品の価格変更情報をオンラインで配信する予定だった。 しかし、システムの不具合で一部の店舗に情報が伝わらず、レジが新しい価格表示に対応しないなどの不具合が起きた。 この影響で全136店のうち首都圏の44店舗を臨時休業にした。 店頭におわびのちらしを張り出したり、店員が口頭で説明した。 広報担当者は「多くのお客さまにご迷惑をおかけして大変申し訳ない」と話している。 2日は全店が通常営業する予定。

記事からは、消費税の扱いそのものではなく、更新プログラムの配信に問題があったと読めるのだが、実際はどうなんだろう。 当然テストをしているだろうから、今回が初めての自動配信ってことはないだろう。 配信対象を管理する部分に問題があって、消費税で変更したモジュールが配信不要と判断されたのだろうか。

こうした配信の手間を排除するために普及してきたWeb化だけど、レジみたいに応答性を求められるものまでWeb化するのは、まだちょっと無理があるんだよな。

ところで消費税だけど、税率をシステム中にどう持たせるのが良いのかな。 って、システムが何を扱っているかで、どう持つ必要があるかも決まってくるのだろうから、一括りに理想を語ることはできないんだろうけどさ。 販売系のシステムなんかは、色々面倒なことがありそうだな。

ということで、ちょっと考えてみた。

単純なのは、定数マスターにでも税率を一つ持っておくことだろうけど、過去だったり切替がはっきりしている未来だったりを扱う必要があると駄目。 一般は10%だけど、贅沢には倍で医療関係は非課税とか、分野毎に税率が違う場合も対応できない。

これらを全部扱えるようにしようとすると、DBに消費税マスターとして持つならこんな感じか。

区分 税率 発効日
一般 0.03 1989/04/01
一般 0.05 1997/04/01
一般 0.08 2014/04/01
一般 0.1 2015/10/01
贅沢 0.2 2015/10/01
医療 0 2015/10/01

業種や贅沢品か必需品かといったことで税率が変わることを考慮して持たせた項目が区分。 同じく、時代によって税率が違うことへの対応が発効日。

と、とりあえず形にしてみたのだが。

表中の贅沢区分は、2015/10/01からは10%だが、これより前は存在しない。 でも、この日より前はきっと一般扱いだったんだよな。 これを、一般と税率は同じだけど贅沢の過去として3%と5%のレコードを登録するか、見つからなかったら一般として扱うことにするか。 暗黙のルールを作らないためには前者なんだろうけど、区分を新規追加した時なんかに整合性を保つのが面倒そうだよな。

一般についても、軸はちょっと違うけど同様の問題がある。 消費税の制度が始まるより前の日付をどう扱うか。 まあ、違う名目の税金がかかっていたんだけど、消費税としては0と考えれば良いのかな。

或る区分の商品の或る日の税率を取得するのに

  1. 指定の区分の指定の日の税率を取得する。
  2. 該当データが無い場合、一般の区分で指定の日の税率を取得する。
  3. 一般にも該当データが無い場合、非課税として0を返す。

とすると、例えば贅沢区分の2014/04/02の税率を取得するSQLはこんな感じか。

select nvl( ( select 税率 from ( select 税率, row_number() over ( order by 発効日 desc ) idx from 消費税 where 区分 = '贅沢' and 発効日 <= to_date('2014/04/02') ) where idx = 1 ), nvl( ( select 税率 from ( select 税率, row_number() over ( order by 発効日 desc ) idx from 消費税 where 区分 = '一般' and 発効日 <= to_date('2014/04/02') ) where idx = 1 ), 0 ) ) from dual

なんかゴチャゴチャしているな。 しかし、ゴチャゴチャしてても実はパラメーターが違うだけでやっていることは同じだったりするので、その同じ部分を関数化してみよう。

指定の区分の指定の日より古い発効日のデータのうち、発効日が最も新しいものの税率を返す。 という関数を定義する。

create function 消費税率 ( p_class in varchar2 , p_date in date ) return number is v_tax_rate number; begin select 税率 into v_tax_rate from ( select 税率, row_number() over ( order by 発効日 desc ) idx from 消費税 where 区分 = p_class and 発効日 <= p_date ) where idx = 1; return v_tax_rate; end; /

これを使うと、さっきのSQLは

select nvl( 消費税率( '贅沢', to_date( '2014/04/02' ) ), nvl( 消費税率( '一般', to_date( '2014/04/02' ) ), 0 ) ) from dual

かなりすっきりした。 第2引数に日付型の値も文字列の日付も許すとか、第2引数が無ければ当日とするとか、パッケージ内で多重定義しておくと色々便利かもしれないな。

指定の条件で見つからなかったら一般区分を見に行くという流れも共通なので、ここまで含めて関数にしてようかとも思ったが、区分についてはほとんどの場合で一般を指定するんだと思い直して、更に考える。

変わるのは消費税であって商品ではないという考えでここまで来たのだが、商品は変わらなくても商品の区分は変わっているのだから、これを商品や商品区分の側で表現しても良いんだよな。

例えば、商品マスターに消費税区分を持たせておいて 「2009/05/29までは一般で翌日から贅沢」 なんて感じで切り替えられるように、別のレコードとして登録するのはどうだろう。

商品番号 商品名 価格 税区分 発効日 失効日
1 カローンの蜘蛛 420 一般 1986/06/10 2009/05/30
2 カローンの蜘蛛 420 贅沢 2009/05/30

商品マスタの新規登録や変更では、対応する税区分が存在しない場合はエラーとする。 2009/05/30から贅沢デビューさせるためには、この日以前の発効日を持つ贅沢区分のデータが消費税マスターに登録されている必要があるのだな。

逆に非課税にするには、こっちの税区分をNULLにするだけで、消費税マスターには非課税のレコードは要らない。

いや、NULLは駄目か。 こんなところにNULLがあるのも色々扱い難いし、テーブルの結合をするときに対称性を崩さないように非課税のレコードを持たせるべきだろう。 発効日を大昔にした非課税のレコードを一つとか。

非課税の持ち方がどうであれ、消費税を取ってくるのに無かったら一般からなんて必要がなくなるし、なんかこっちの方が自然な気がしてきた。

と思ったが、改めて商品マスターの登録例を見ると不自然な気がするな。 同じ商品名が並んで正規化されていないように見えるからだろうか。 まあ、実体は同じものを違う区分で扱うための方便だし、これはたぶん特例なんだけど、特例かどうかに関係無く有効期限が設定できるなら履歴のテーブルを作って失効分はそっちに移せよって気もするな。 商品マスターなんて大抵は膨れ上がるものだし、スリムにできるならスリムにした方が良いだろう。 ほとんどの場合、見たいのは今有効な商品だろうし。

商品マスターを今生きてるものと履歴に分けるとして、消費税はどうだろう。 こっちはデータの数も大したことはないし、目的からしてもきっと過去分もあった方がいいのだが、最も多い使い方はきっと現時点の消費税の取得なんだよな。 さっきは、overloadできれば便利だと思っただけで放置だったけど、やっぱり今日の消費税を取得する関数があった方がいいのかな。

create function 今日の消費税率 ( p_class in varchar2 ) return number is begin return 消費税率( p_class, sysdate ); end; /

いや、特定の区分の値を取り出すだけならこれでもいいけど、商品マスターから税込み価格と一緒に大量データを取得する場合を考えると、現時点のデータを見るためのVIEWがあった方がいいのか。

create view 今日の消費税 ( 区分, 税率 ) as select 区分, 税率 from ( select 区分, 税率, row_number() over ( partition by 区分 order by 発効日 desc ) idx from 消費税 where 発効日 <= sysdate ) where idx = 1 ;

商品マスターから税込み価格と一緒に全データを取得する場合、関数を使うとこんな感じ。

select 商品名 , 価格 , 税区分 , trunc( 価格 * ( 1 + 今日の消費税率( 税区分 ) ) ) as 税込み価格 from 商品 ;

VIEWと結合させるとこんな感じ。

select 商品名 , 価格 , 税区分 , trunc( 価格 * ( 1 + 税率 ) ) as 税込み価格 from 商品 inner join 今日の消費税 on 商品.税区分 = 今日の消費税.区分 ;

性能面で有利なのはVIEWの方だろうけど、見た目に判り易いのは関数を使った方か。

しかしここまで来ると、定数マスターに税率を一つだけ持っているのと変わらないな。 面倒だろうと思って始めたのに、道具さえ準備すれば大して面倒じゃないという結論になりそうだ。

いや,違う違う。 SQLがすっきりしてきたので変な満足感が沸いてきたけど、そんな自分に騙されちゃ駄目だ。 道具の準備が面倒だって話だったんだよな。 実際、今日のたったこれだけの思考過程ですら、二転三転してるのだし。

これに業務要件が絡んでくると、面倒臭さも掛け算で増加するんだろう。 例えば、コンビニ業界なんかで 「消費税の切替は営業日を基準にします。 営業日の最終時刻は27時です」 なんて。 しかも、よくよく聞いてみると、その最終時刻がずっと同じではなくて、時代によって23時から24時になり27時に至るとか、平日は27時だけど休日は25時で年末は24時とか。

まあ、面倒なのが決して悪いことばかりじゃないんだけどさ。 税率が変わるのも複数あるのも面倒くさいけど、面倒だからこそ仕事になったりもするのだし。 払う立場から言えば、必需品は安くしてほしいしね。

いやいや、仕事になっても、やりたい仕事かどうかは別の話か。 道具さえ準備すれば大して面倒じゃない仕事ってのは、発注する立場なら 「道具があるから簡単でしょ?」 と言うだろうし、受注する立場なら 「その道具の準備が作業の中核なんですよ」 なんて言うだろうし、システムそのものとは違うところで面倒になりそうな気がするな。

そういえば、消費税に反対する人達の言う最大の反対理由が逆進性なのだが、これはどうなんだろう。 言い換えると累進課税が良いとなるのだが、俺にはどうもその根拠というか正当性がはっきりしない。 金持ちはきっと悪いことをして金を儲けているに違いないといった貧乏人の僻みに乗っかって、取りやすいところから税を取ってるだけじゃないか。 ついでに人気も取れたりするし。

それが助け合いって言うのは聞こえが良いけど、税金は強制であって厚意じゃないんだよな。 それに、助ける側と助けられる側って、大抵の場合は固定化してほぼ一方通行だったりするし。 助けさせる立場の人の取り分が一番多かったりした日には、ねえ。

かといって、払う税金のバランスに応じた助け合いが実現しないことの補填のための制度を作ったりすると、これはこれで反対の嵐だったりするんだろう。 「生活保護受給者は、納税額500万以上の者の家事サービスを週1回無料で実施すること」 なんて、議題に上げただけで議員の首が飛びそうだ。

貧乏人に直接何かさせるのではなくて、我慢させるとかサービスレベルに差を付けるのも難しい気がするな。 例えば救急車や救急病院は金持ちを優先すると明文化するとか。 これは、言わないだけで実はそうなってるのかな。