[Перевод] Двойная бухгалтерская запись в реляционной БД

От переводчика: в ходе моей работы в нигерийском финтехе пришлось мне создавать с нуля одну платежную систему. Я тогда ничего толком не понимал в вопросах бухгалтерии, в том как именно лучше хранить платежи и балансы. Но было подозрение, что примитивный вариант с одной циферкой баланса в аккаунте пользователя слишком прост чтобы быть правильным.

Разобраться и избежать кучи граблей в этом деле мне помогла данная статья. При этом информации по теме «как сделать свою платежную систему» довольно мало, а в учебниках по бухучету программисту сходу разобраться не так просто (и очень нудно). Надеюсь, этот материал окажется полезным тем, кто только собирается что-то такое делать.

Сразу извиняюсь за возможные неточности в русскоязычных финансовых терминах — я все-таки программист, а не бухгалтер, и с русской терминологией в этой сфере недостаточно знаком.


Введение

Многие компьютерные системы, использующие реляционные БД, хранят в них какую-то финансовую информацию о балансах и транзакциях. При этом при проектировании и разработке такой БД часто встает вопрос, а как именно хранить эту информацию. Обычно выбор стоит между дешевой «простой записью» и более сложной «двойной записью».

jmgvsgsfjrriyu-vzj-mdzacmvy.jpeg
Лука Пачоли, автор самой старой (15 век) дошедшей до нас книги с описанием принципов двойной записи

В системе с «простой записью» числовые значения записываются только один раз. В системе с «двойной записью» каждое значение записывается дважды, как кредит (положительное значение) и как дебит (отрицательное значение). При этом есть набор правил, определяющих связь между этими значениями. Эти правила вам легко опишет любой опытный бухгалтер, хотя он может и не представлять, как именно они могут быть представлены в реляционной БД.

Основные правила таковы:


  1. Каждая запись в системе должна быть сбалансированной, т.е. сумма всех значений в рамках одной операции должна давать ноль.
  2. Сумма всех значений во всей системе в любой момент времени должна давать ноль (правило т.н. «пробного баланса»).
  3. Уже занесенные в БД значения нельзя редактировать или удалять. При необходимости исправлений операция сперва должна быть отменена другой операцией с противоположным знаком, а затем повторена с правильным значением. Это позволяет реализовать надежный аудиторский след (полный лог всех транзакций, часто требуемый при проверках).


Применимость двойной бухгалтерии

На старте проекта низкая цена простой записи всегда искушает, а стоимость реализации и сложность полноценной двойной записи кажется излишней. Однако в действительности часто использование простой записи оказывается ложной экономией.

Если бухгалтерская информация в ИТ системе лишь копирует существующую бумажную отчетность, хранящуюся вне разрабатываемой БД, тогда простая запись еще имеет право на жизнь. Однако если хотя бы один из перечисленных ниже фактов о системе правдив, то с самого начала стоит использовать двойную запись:


  1. Если когда-либо потребуется бухгалтерский аудит информации
  2. Если информация в системе — единственный источник сведений о собственности
  3. Если информация касается объектов, имеющих высокую ценность
  4. Если систему планируется серьезно развивать в дальнейшем


Пример двойной записи

Ключевой идеей двойной записи является существование особого «cash book» аккаунта (прим. перев.: не нашел как это адекватно назвать по-русски, может кто подскажет? ). Этот аккаунт содержит записи, сделанные когда ценности (например, деньги) вносятся или выводятся из нашей бухгалтерской системы. Таким образом, текущий баланс этого аккаунта отражает общее количество ценностей в системе.

Дальше показан простой пример с двумя аккаунтами, «cash book» и «Смит».

(а) £300 вводится в систему и кладется на счет Смита. Создается кредит на £300 в аккаунте Смита (кредит справа, дебит слева). Чтобы уровнять эту сумму, создается дебит на £300 в аккаунте cash book.

wkicpxkfio9jnzpj22rujmftpww.gif

(b) Затем Смит выводит £50 из системы. Создаем дебит на эту сумму в аккаунте Смита и кредит в cash book.

nensc8s2cbhjyh9wbpnakjl1dze.gif

(c) Добавим еще один аккаунт Паттел и переведем 100£ ему от Смита. Для этого нам понадобится создать дебит на эту сумму у Смита и кредит у Паттела.

(d) В качестве завершающего штриха пусть теперь Паттел выведет из системы 60£. Мы создаем дебит в его аккаунте и кредит в Cash book.

sfh_fcvhtlljtfbssjhysasppdm.gif

В результате всех этих операций мы можем подсчитать, что итоговый баланс Смита 150£, Паттела 40£, а в Cash Book -190£, отрицательная сумма балансов всех остальных аккаунтов. Основываясь на этих простых правилах и операциях в дальнейшем можно построить очень комплексные системы контроля ценностями.


Модель данных

Структура простой модели данных, которая может использоваться для представления всей этой информации:

w7o8rrkhg_ovqkwls1kczhyd0se.gif

Таблица POSTING содержит сами двойные записи. Хранение всех цифр в одной таблице сильно упрощает все вычисления. В качестве первичного ключа стоит использовать монотонно возрастающий счетчик. Значения при этом должны идти подряд, в таком случае по номеру всегда можно будет убедиться, что ни одна запись не была удалена. Таблицы BATCH и JOURNAL используются для контроля и ввода данных в эту таблицу POSTING.

Каждая запись в таблице JOURNAL представляет транзакцию (с точки зрения бизнеса), которая генерирует двойные записи. Такая транзакция — это завершенная единица работы или какого-либо бизнес-процесса. Либо все POSTING записи, ассоциированные с JOURNAL записью должны быть успешно завершены, либо ни одна из них. Сумма всех POSTING записей в рамках одной транзакции должна давать ноль. Каждая операция по переводу средств из примера выше представляется своей записью в таблице JOURNAL

Запись в таблице BATCH сделана для удобства ввода данных. Она используется для группировки записей JOURNAL в удобные пакеты, например набор чеков для ввода в систему, какой-то глобальный бизнес-процесс вроде начисления процентов сразу всем пользователям и т.п.

Таблица ACCOUNT хранит данные о владельцах ценностей в системе.

Таблица ASSET TYPE содержит информацию о типах ценностей, использующихся в системе. Добавив тип ценности в первичный ключ таблицы POSTING можно сделать систему, оперирующую сразу несколькими видами ценностей (например, обработку нескольких валют).

Вот как может выглядеть такая БД для примера выше в максимально упрощенном виде:

lbjggdyjidzxk9f-kcode7t-63m.png

Баланс столбца Amount в таблице POSTING всегда равен нулю после завершения любой транзакции из JOURNAL (ПО должно гарантировать отсутствие записи незавершенных транзакций в БД).

Сумма операций для аккаунта Cash Book дает -190, что равно сумме балансов Смита и Паттела с обратным знаком.

Для демонстрации работы с многовалютностью был добавлен новый вид ценности. Если Смит хочет поменять 20 фунтов на доллары по курсу 1 за 1.5, транзакция будет проведена через Cash Book таким образом:

5nbakjfpvja2lvyvujou-zkqjfo.png


Расчетные периоды

Модель, которую мы получили, отлично выглядит, но в реальности очень быстро сломается под высокой нагрузкой из-за того, что мы не можем ничего удалять и вынуждены постоянно пересчитывать все возрастающее количество записей в POSTING.

Большинство систем бухучета имеют понятие расчетного периода — обычно это месяц, три месяца или год. Такой период подсказывает удобные моменты для разделения потока данных. Обычно удобной точкой является конец года, календарного или финансового.

Мы можем добавить столбец с индикатором периода к таблице POSTING и к ее первичному ключу, разбив данные на группы, которые могут обрабатываться независимо. Если бы в примере выше часть записей попала на новый расчетный период, балансы аккаунтов были бы перенесены следующим образом.

Сперва были бы очищены балансы предыдущего периода.

1wfoxv4wlpwkibojtct8ybgz8bs.png

А затем они были бы перенесены в новый период

5nbakjfpvja2lvyvujou-zkqjfo.png

После определенного времени все записи периода YEAR 1 могут быть отправлены в архив и удалены из системы без потери ее целостности.


Агрегирование транзакций

Некоторые операции в бухгалтерской системе могут затрагивать сразу многих, или вообще всех пользователей. Например, выплата процентов всем пользователям в виде доли от их текущего баланса.

Такие операции могут быть обработаны в рамках одной транзакции в таблице JOURNAL и можно агрегировать все операции с Cash Book в одну общую запись в таблице POSTING (вместо создания отдельной операции для каждого аккаунта). Это позволит соблюсти все перечисленные выше правила бухгалтерского учета и при этом сократить в два раза количество записей в БД. При использовании такого подхода конец года в БД будет выглядеть так:

fyoupeludonzc1ar2qdyvzlss0m.png


Пакетная обработка

Пакетная обработка часто используется для упрощения ввода данных в бухгалтерскую систему.

Исторически так работала обработка чеков. Бухгалтеру выдавалась пачка из десяти чеков, номер пачки и общая сумма всех чеков. На первом этапе чеки вносятся в систему в виде «неавторизованных» записей. При этом через таблицу BATCH проверяется их количество и общая сумма, и только если они совпадают с правильным значением пользователю позволяется закоммитить пачку. После того как это сделано, пачка отправляется другому сотруднику, который проверяет ее на валидность и затем «авторизует» если все введено верно.

Такой процесс называется «maker/checker» и может использоваться для ввода любых значимых данных в систему.

Правильным при этом будет хранить «неавторизованные» записи в отдельной таблице от основного набора двойных записей в таблице POSTING. Также можно иметь целый ряд таких таблиц для разных бизнес-процессов. Например, в случае с чеками, через которые осуществляется ввод или вывод денег из системы, бухгалтеру надо будет проверить только один аккаунт. Так как второй, Cash Book, в таких операциях всегда подразумевается неявно. В таком случае в таблице CHEQUE можно будет обойтись только одним столбцом с аккаунтом, в то время как в гипотетической таблице FUND TRANSFER нужно будет два столбца: «отправитель» и «получатель».

Именно тут возникает основное непонимание принципов двойной записи. Большинство людей в обычной жизни встречают только простые бумажные книги учета. В такой бумажной книге, например для учета финансов какого-то клуба по интересам, нужна только одна запись для каждой операции. Однако в ней по-прежнему есть неявная двойная запись, так как всегда неявно присутствует Cash Book аккаунт (в данном случае — этот самый клуб), ведь все движения средств это всегда или ввод (оплата взносов участниками), или вывод денег из системы (траты клуба).

Вторая причина заблуждений — то что в личных выписках по счету деньги, положенные на счет будут считаться «кредитом», потому что человек по сути кредитует банк, который получает его деньги. Хотя если бы этот человек вел свою бухгалтерскую книгу, эта запись была бы в ней учтена как «дебит» — так как банк должен эти деньги своему клиенту. Эти деньги выводятся из «платежной системы» пользователя и вводятся в систему банка.


Архитектура программной части

ПО, реализующее такую бухгалтерскую систему с двойной записью, лучше всего разрабатывать с помощью ООП и многоуровневого подхода. Уровни получаются такими:


  1. Внешний интерфейс
  2. Бизнес-логика
  3. Работа с БД

Конечно, архитектура системы будет зависеть от того, что именно эта система должна делать, однако можно предположить наличие в ней следующих модулей:

PostEntry: модуль, который управляет созданием двойных записей в таблице POSTING. Он отвечает за вставку записей, присвоение ID и таймстампов. Модуль не может удалять или изменять записи и никакой другой модуль не должен удалять или изменять эти записи, за исключением возможного удаления старых архивированных записей для уже неактуальных расчетных периодов. Таблица POSTING должна быть доступна только на чтение для всех других модулей.

MakeDeposit, MakeWithdrawal, MakeTransfer: эти модули реализуют базовую бизнес-логику для операций переводов средств. Они будут использовать модуль PostEntry для занесения своих результатов в БД

ChequeEntry и ChequeAuthorisation, ReceiveBACS (прим перев: BACS — система межбанковских платежей): эти модули будут связывать систему с внешним миром и предоставят высокоуровневый интерфейс. Они будут использовать модули бизнес-слоя для выполнения своих функций. В таком случае можно гарантировать правильность процессинга вне зависимости от метода ввода данных, так как и ChequeEntry и ReceiveBACS будут работать через тот же самый MakeDeposit

Эту методологию разделения слоев можно применять в большей или меньшей степени, в зависимости от сложности системы и желаемой чистоты использования принципов объектного дизайна. При этом может иметь смысл, например, разрешить модулю генерации отчетов (например TestTrialBalance) прямой доступ к БД с уровня интерфейса — вместо того чтобы создавать промежуточные модули на бизнес и БД слоях.

uss7ykm8aquutbkj2_jtjp8jrly.gif


Пробный баланс

«Пробный баланс» — главный способ проверки целостности бухгалтерской системы. Если все записи вносились в систему по правилам двойной записи и не было никаких ошибок, в таком случае сумма всех записей должна давать ноль. Вероятность того, что несколько отдельных ошибок сложатся в и сумме дадут ноль на невалидной базе обычно настолько мала, что ей пренебрегают.

Лучшим способом проверки является последовательное движение от верхнего уровня к нижнему. Проверки имеет смысл выполнять в таком порядке:


  1. Сумма всех значений в столбце POSTING.Amount
    Если найдена ошибка (значение не ноль), то:
  2. Сумма всех значений POSTING.Amount, но отдельно посчитанная для разных типов ценностей и расчетных периодов
    На этом этапе должно стать яснее, в какой части системы произошла ошибка.
  3. Проверка отдельных операций в таблице JOURNAL. Поскольку сумма всех POSTING.Account в каждой транзакции из таблицы JOURNAL должна тоже давать ноль, дальше можно отследить конкретную проблемную транзакцию.


Типы записей в JOURNAL

Таблица JOURNAL содержит простое представление сущностей, которые однако часто оказываются на деле более сложными и вовлеченными в различные отношения.

Иногда имеет смысл разбить одну таблицу на несколько. Например на MATERIALISED и DEMATERIALISED, которые могут иметь различный набор столбцов, например для материальных сущностей могут потребоваться данные об их текущем местоположении.

Или же в одной таблице могут храниться различные подтипы ценностей, такие как валюта или ценные бумаги, у каждого подтипа при этом может быть свой набор свойств и атрибутов.

Сущности, имеющие как под- так и надтипы могут организовываться в БД одним из четырех способов (это довольно стандартная ситуация для любой БД):


  1. Одна общая большая таблица с множеством необязательных столбцов для атрибутов подтипов
  2. Отдельная таблица для каждого подтипа, с дублированием всех общих столбцов
  3. Разделение сущностей таким образом, чтобы надтип хранился в отдельной таблице и джоинился с другими таблицами, содержащими только специфичные для подтипов столбцы
  4. Так же как в 3, но с дублированием столбцов супертипа в таблицах подтипов

У каждого из четырех вариантов есть свои плюсы и минусы. С точки зрения двойной записи полезно иметь общую таблицу для POSTING записей. Вариант 1 лучше подходит для простой бухгалтерской системы (как в примерах в данной статье, где единственное отличие в типах ценностей определяется столбцом JOURNAL.Type). Вариант 3, вероятно, лучше подходит для сложных систем, работающих с широким спектром сильно отличающихся ценностей.

© Habrahabr.ru