この記事ははじめてのアドベントカレンダー Advent Calendar 2023 23日目の記事です。
こんにちは。DIGGLEのCTO水上です。アドカレのために何を書こうか悩んでいたら投稿前日になってしまいました。
悩んだ末、社内勉強会で一度話した内容を書くことにしました。(省力化・・)
まえがき
フリーランス等で、少し複式簿記をかじったことがある方は、とりあえずネットで調べた方法で仕訳を書いてみたりしたものの、結局本質的に何をやっているのかがよく分からないという方も多いのではないでしょうか。
私も過去にフリーランスだった時期があり、自分で理屈を理解するためにスプレッドシートで実装してみたりしていました。色々と書いてみた経験を通して、私は自分なりに本質的な仕組みがどういうものなのかを理解することができましたが、軽く触れてもそれを理解しづらかった要因として、検索しても普遍的な仕組みの説明にたどりつくことはほぼなく、財務的なルールに関する記事がほとんどだからではないかと感じています。
そこで、(私の知る限りで)複式簿記のシステムのうち最も基礎となる部分だけにフォーカスして、技術的(あるいは数理的)に抽象化して切り離し、データベース上で実装することを通じて、エンジニアにとって馴染み深い形で仕組みを理解する手助けになるような記事を書いてみました。
※ その道の専門家ではないので、細かい部分での誤りがありましたらご指摘いただけると幸いです。
定義と設計
データベース上で実装するといっても、設計なしには何も記述できません。まずはいくつかの概念を定義しながら、具体の実装方針を検討していきます。
※定義が難解に感じるようであれば、実装の部分まで読み飛ばしていただいて構いません
お金を定義する
ひとまずお金という概念を定義します。お金は、
- 定量的に認識できる(つまり、100や200など私たちの知っている数字表記で表せるということ)
- この世界のお金の総量は、増えたり減ったりしない
という性質をみたすものとします。
総量が変わらないという制約はともかく、技術的にはInteger型を採用することにすれば要件を概ね満たすものとなるでしょう。実は、総量が変わらないという制約は複式簿記と最も大きく関係する特徴ですが、それは最後に触れます。
箱を定義する
つぎに、 箱 を定義します。箱は、
- 箱の中には、お金が無数に存在し(しかし無限ではない!)、誰かがそこからお金を動かしたとしても無くなるようなことはない
- この世界のすべてのお金は、いつもどれか一つの箱に入っている
- この世界には箱が複数存在できる
とします。
各箱は、常に無数のお金が入っているわけですが、操作をして底が尽きることがないということを考えて、初期値を0と考え、マイナス値になっても扱えるということにしてしまいましょう。(体重計に箱を載せた状態でONにするようなイメージ)
たとえば世界に箱が3つしかないとき、その箱の全体は、(0, 0, 0)
と表すことができそうです。ただ、単に底を突く心配をしなくても良いように扱いたいだけなので、初期値がいくらなのかというのは重要ではありません。
(お金の)移動を定義する
次に、箱から箱へのお金の移動について定義します。移動は
で定められます。
素朴にコードに落としてみるとすると、イメージとしては { from: Box, to: Box, amount: Integer }
のような構造体が想像できると思います。実装上はこの方式を採用します。
一方で、別の表現方法として、各箱の増減分を並べたものでも表すことができます。たとえば、3つ箱がある世界で、1つめの箱から2つめの箱へ、50移動することを表すには (-50, +50, 0)
と表すことができます。箱と同じようにInteger型のタプルで表現可能になります。数学でいう点とベクトルみたいなものですね。
集計結果を定義する
移動が何度も起きるわけですが、結果として各箱がどれくらい増減したのか、というものを 集計結果 ということにします。
集計結果というものを具体的にどういうユースケースで、何を意味するものとして使うのかは後半の例で紹介します。
ここで、移動をタプル=ベクトルで表現できるメリットとして、ベクトルの自然な加法に基づいて、足し合わせを表現することができる点があります。つまり移動Aと移動Bを両方行うことを、単にA+Bで表現できます。実際、1つめの箱から2つめの箱へ、50移動し、2つめの箱から3つめの箱へ、30移動することは (-50, +50, 0) + (0, -30, +30) = (-50, +20, +30)
で表すことができます。その意味で、移動の足し合わせもまた広義には移動ということにします。この世界のお金が増えたり減ったりしないという条件は、この移動を表すベクトルの要素の総和が0になっていることから保証されます。
上記の (-50, +20, +30)
という移動は、それぞれの箱について50減少、20増加、30増加という増減があったということになります。
後のために少しだけ符号の扱いについて取り決めておきます。
- ある集計結果を ある箱の 増加分 であると定義する場合、移動の合計を計算した上で、当該の箱の増減値そのものを指すとします
- ある集計結果を ある箱の 減少分 であると定義する場合、移動の合計を計算した上で、当該の箱の増減値から符号を反転した値を指すものとします
実装上では移動はタプルではなく { from: Box, to: Box, amount: Integer }
の表現形式を使うので、これを足し合わせた結果に相当する集計結果は少しややこしくなりますが、下記の擬似コードのようなイメージで計算します。
N個目の箱の増加分 の場合
箱に入ってきた分(to)から出ていった分(from)を引きます
擬似コード例: moves.filter(m => m.to == N).sum() - moves.filter(m => m.from == N).sum()
N個目の箱の減少分 の場合
箱から出ていった分(from)から箱に入ってきた分(to)を引きます
擬似コード例: moves.filter(m => m.from == N).sum() - moves.filter(m => m.to == N).sum()
実装する
お金はInteger、そして箱は識別可能ならば十分であることを考えて、箱は単に文字列リテラルで表現するとしましょう。そうすると、さっそく移動から実装し始められます。
CREATE TABLE moves
( "from" TEXT NOT NULL
, "to" TEXT NOT NULL
, "amount" INTEGER NOT NULL
)
;
集計結果は、次のようなSQLで表現できます。
箱"foo"の増加分 の場合
SELECT
( (SELECT SUM(amount) FROM moves m WHERE m."to" = 'foo')
- (SELECT SUM(amount) FROM moves m WHERE m."from" = 'foo')
)
箱"foo"の減少分 の場合
SELECT
( (SELECT SUM(amount) FROM moves m WHERE m."from" = 'foo')
- (SELECT SUM(amount) FROM moves m WHERE m."to" = 'foo')
)
以上で実装の基本形は完了です!
ここまでの実装だけでは、一体何が実現できるのか全くわかりません。そこで、次から簡単な例をもとに模擬的なデータを構成してみます。
例①:3つの箱でP/L(損益計算書)を表現する
P/L(損益計算書)とは、ある期間における収益と費用、そしてその差し引きの利益を表示した表のことです。
まずは、3つの箱 売上、現金、 費用 を用意します。
次に移動データを使って、事業活動を次のルールで表現してみましょう。
- 売上が発生した場合、 売上 から 現金 への移動 で表現する
- 費用が発生した場合、 現金 から 費用 への移動 で表現する
以下はサンプルのデータです。
(摘要欄のようなカラムがないので、代わりにコメントを入れていますが、どのようなデータであるか何となくイメージできると思います。)
INSERT INTO moves("from", "to", "amount")
VALUES
('現金', '費用', 600)
, ('現金', '費用', 500)
, ('売上', '現金', 200)
, ('売上', '現金', 300)
, ('売上', '現金', 200)
, ('売上', '現金', 200)
, ('売上', '現金', 500)
, ('売上', '現金', 200)
, ('売上', '現金', 300)
, ('現金', '費用', 600)
;
最後に、簡単なP/Lとして、収益、コストを集計結果として以下で定義します。
- 収益 = 売上 の箱 の 減少 分
- コスト = 費用 の箱 の 増加 分
使いまわしやすくするためにVIEWにしてしまいましょう。
CREATE VIEW sales(value) AS
SELECT
( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '売上')
- (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '売上')
) AS value
;
CREATE VIEW cost(value) AS
SELECT
( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '費用')
- (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '費用')
) AS value
;
最後に上記の差し引きとして利益を算出してみます。結果を整形すると次のようになります。
しかし、ここまでの話ならば、箱の移動などと面倒なことを考えず、単に売上と費用をそれぞれ別々に計上すればよさそうですよね。そこで次の例を紹介します。
例②:5つの箱でB/S(貸借対照表)を表現する
B/S(貸借対照表)とは、ある時点における資産の内訳と資産の帰属する先(あるいは、調達方法)をそれぞれ左右に並べた表のことです。この表の左右それぞれの合計は一致(バランス)するべきで、資金の提供元(右側)から見て、資金が現在どういう状態になっているのか(左側)を表します。
前の例と同じ要領で、5つの箱 売上、費用、資産、負債、純資産 の箱を用意します。
事業活動の内容を次のように表現します。
- 売上が発生した場合、 売上 から 資産 への移動 で表現する
- 費用が発生した場合、 資産 から 費用 への移動 で表現する
- (銀行などからの借り入れにより)資金の貸し手に帰属する資産が発生した場合、 負債 から 資産 への移動 で表現する
- (資本金や株式による調達により)株主に帰属する資産が発生した場合、 純資産 から 資産 への移動 で表現する
以下はサンプルのデータです。
INSERT INTO moves("from", "to", "amount")
VALUES
('純資産', '資産', 10000000)
, ('負債', '資産', 5000000)
, ('資産', '費用', 3000000)
, ('売上', '資産', 1000000)
, ('資産', '費用', 50000)
, ('資産', '費用', 1500000)
, ('売上', '資産', 1300000)
, ('資産', '費用', 50000)
, ('資産', '費用', 1200000)
, ('売上', '資産', 2000000)
, ('資産', '費用', 50000)
;
以上を用いて、ひとまずP/Lを出力してみましょう。実は、前の章のP/Lを出力するときのSQLから形を変える必要は一切ありません。
収益 | 4,300,000 |
コスト | 5,850,000 |
利益 | -1,550,000 |
これに加えて、いわゆるB/Sを作ってみます。集計結果を次のように定義します。
- 資産 = 資産 の箱 の増加 分
- 負債 = 負債 の箱 の 減少 分
- 純資産 = 純資産 の箱 の 減少 分
同じくVIEWを作ってみます。
CREATE VIEW assets(value) AS
SELECT
( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '資産')
- (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '資産')
) AS value
;
CREATE VIEW liabilities(value) AS
SELECT
( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '負債')
- (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '負債')
) AS value
;
CREATE VIEW equities(value) AS
SELECT
( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '純資産')
- (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '純資産')
) AS value
;
これらの結果を集計すると、次のようになります。
資産 | 13,450,000 | 負債 | 5,000,000 |
純資産 | 10,000,000 |
資産合計 | 13,450,000 | 負債+純資産合計 | 15,000,000 |
はい、バランスしていませんね。差額の1,550,000はどこにいるかというと、実はP/L側の利益にいます。P/Lで利益がマイナスであるということは、純資産が縮小しなければいけません。そこで、P/Lの利益は純資産に振り替える(一般には利益剰余金という純資産科目に)移動が必要があります。このためのルールを新たに2つ追加します。この移動は、実際に決算振替仕訳と言われています。
- 売上を確定する場合、 純資産 から 売上 への移動で表現する
- 費用を確定する場合、 費用 から 純資産 への移動で表現する
売上と費用を確定させるため、P/Lの結果を参照しながら次のようなデータを作成します。
INSERT INTO moves("from", "to", "amount")
VALUES
('純資産', '売上', 4300000)
, ('費用', '純資産', 5850000)
;
上記を追加した上で、改めてP/LとB/Sを計算してみると、次のようになります。
P/L
B/S
資産 | 13,450,000 | 負債 | 5,000,000 |
純資産 | 8,450,000 |
資産合計 | 13,450,000 | 負債+純資産合計 | 13,450,000 |
これでB/Sがバランスしました。一方でP/Lが0になってしまいました。
変に思うかもしれませんが、もし再度P/Lを表示したい場合には、決算振替仕訳を含めないで集計すればいいだけなので、P/LとB/Sを表示するという観点からいえば、これで十分といえるでしょう。
複式簿記との関係性
実は、この記事で定義した 移動 が複式簿記における 仕訳 と同じものになります。さらに 移動元 、移動先 はそれぞれ 貸方 と 借方 に相当します。(貸し手から借り手への移動だと考えると、分かりやすいですよね)
通常の簿記ではより細かく勘定科目が分かれているものですが、単に箱を細分化していくだけで同じことを説明することができます。
さらに、 貸借平均の原理 とよばれる次の式があります。
資産 + 費用 = 負債 + 純資産 + 収益
ここで実は、左辺に来ているものは、箱の 増加 分で、右辺に来ているものは、箱の 減少 分として本記事において定義したものでした。減少分であるということは符号を反転させれば増加分を表しているわけなので
資産 + 費用 + (-負債) + (-純資産) + (-収益) = 0
と書き換えると、これは各箱からの(広義の)移動があったとしても、お金が増えたり減ったりはしないという制約条件そのものを指していると言えます。
まとめ
それで、一体このアーキテクチャのなにが嬉しいのでしょうか?まず一つが、P/LもB/Sも、この仕訳の集合体を唯一の入力として写像できるという点にあります。今回例には載せていませんが、集計の仕方や箱の設定を工夫するだけで、例えばP/Lといっても発生主義・実現主義・現金主義など、それぞれ微妙に異なる見方に写像できたりします。このように、入力データのシンプルさを保ちつつ、多くのアウトプットに対応することができるのが強い利点になります。もう一つは一貫性を持てることです。売上の帳簿と財布の帳簿がそれぞれ別々のデータだと、売上の発生と財布への追加が一致しているようにデータを担保しないといけませんが、全てにおいて移動を根拠にしている場合はそのような心配をする必要はありません。
実は、複式簿記の本質は、開発者にとっては単に自分自身で会計処理をする際の理解をスムーズにできることだけではなく、一つのデータ表現手段として他の領域にも設計上応用できる可能性を秘めていると思います。例えば、お金以外の定量データの場面や、フローとストックの表現が必要な場面などがあります。
さいごに
この記事で紹介している実装をsqliteで実行可能な形でGitHub上に公開しておきました。
GitHub - mizukami234/simple-DEB
DIGGLEでは、管理会計という(複式簿記とは遠からずも近からずな)領域において、最高のプロダクトを作るメンバーを募集しています。興味がありましたら、是非お声がけください!
herp.careers