Содержание цикла статей: https://github.com/nomhoi/empire-erp.
Сегодня начнем рассматривать аналитический учет.
Настройка проекта
Клонируем проект с гитхаба:
git clone https://github.com/nomhoi/empire-erp.git
Заходим в папку reaserch/day3/.
Запустим базу данных и выполним тесты:
docker-compose run test
Подключимся к базе данных empire-erp:
docker exec -it db psql -U postgres -d empire-erp
Step 1. Простые счета
Счета без субсчетов называются простыми счетами. На этом шаге повторим получение оборотов по счетам с использованием главной книги.
Выполним команду в командной строке psql для инициализации базы данных:
empire-erp=# \i step1.sql
Файл step1.sql:
DROP TABLE IF EXISTS general_journal;
CREATE TABLE general_journal(
id serial,
debit_id smallint NOT NULL,
credit_id smallint NOT NULL,
amount money NOT NULL
);
Создаем журнал проводок general journal. В прошлой статье ошибка, на самом деле это журнал проводок, а не главная книга.
Заполняем журнал проводок какими-нибудь исходными данными:
INSERT INTO general_journal(debit_id, credit_id, amount)
VALUES (1, 12, 100.00),
(1, 6, 120.00),
(12, 1, 20.00);
INSERT 0 3
Выводим содержимое журнала:
SELECT *
FROM general_journal
ORDER BY id;
id | debit_id | credit_id | amount
----+----------+-----------+---------
1 | 1 | 12 | $100.00
2 | 1 | 6 | $120.00
3 | 12 | 1 | $20.00
(3 rows)
Получаем из журнала проводок главную книгу и сохраняем ее в таблице general_ledger:
DROP TABLE IF EXISTS general_ledger;
SELECT id AS general_journal_id,
debit_id AS account_id,
credit_id AS corresp_id,
amount AS debit_amount,
( 0.00 ) :: money AS credit_amount
INTO general_ledger
FROM general_journal
UNION
SELECT id AS general_journal_id,
credit_id AS account_id,
debit_id AS corresp_id,
( 0.00 ) :: money AS debit_amount,
amount AS credit_amount
FROM general_journal
ORDER BY general_journal_id;
DROP TABLE
SELECT 6
Выводим содержимое главной книги:
SELECT *
FROM general_ledger
ORDER BY general_journal_id;
general_journal_id | account_id | corresp_id | debit_amount | credit_amount
--------------------+------------+------------+--------------+---------------
1 | 1 | 12 | $100.00 | $0.00
1 | 12 | 1 | $0.00 | $100.00
2 | 1 | 6 | $120.00 | $0.00
2 | 6 | 1 | $0.00 | $120.00
3 | 1 | 12 | $0.00 | $20.00
3 | 12 | 1 | $20.00 | $0.00
(6 rows)
Обороты по счетам:
SELECT account_id,
sum(debit_amount) AS debit_turnout,
sum(credit_amount) AS credit_turnout
FROM general_ledger
GROUP BY account_id
ORDER BY account_id;
account_id | debit_turnout | credit_turnout
------------+---------------+----------------
1 | $220.00 | $20.00
6 | $0.00 | $120.00
12 | $20.00 | $100.00
(3 rows)
Step 2. Сложные счета
Cчета с субсчетами называются сложными счетами. Напомню, что по нашим условиям счета 1-4 являются активными, 5-8 — активно-пассивными и 9-12 — пассивными. Введем дополнительное условие: каждый четный счет является сложным счетом.
Файл step2.sql. Создаем журнал проводок general journal содержащий субсчета:
DROP TABLE IF EXISTS general_journal;
CREATE TABLE general_journal(
id serial,
debit_id smallint NOT NULL,
debit_sub_id smallint,
credit_id smallint NOT NULL,
credit_sub_id smallint,
amount money NOT NULL
);
Заполняем журнал проводок какими-нибудь исходными данными:
INSERT INTO general_journal(debit_id, debit_sub_id, credit_id, credit_sub_id, amount)
VALUES ( 1, NULL, 12, 1, 100.00),
( 1, NULL, 12, 2, 140.00),
( 1, NULL, 6, 1, 120.00),
( 6, 2, 1, NULL, 80.00),
(12, 1, 1, NULL, 20.00),
(12, 2, 1, NULL, 40.00);
INSERT 0 6
Здесь мы должны помнить, что в сложных счетах субсчет не может принимать значение NULL, а в простых счетах значение субсчета всегда должно быть равным NULL. Можно, например, добавить триггер, который будет проверять этот момент.
Выводим содержимое журнала:
SELECT *
FROM general_journal
ORDER BY id;
id | debit_id | debit_sub_id | credit_id | credit_sub_id | amount
----+----------+--------------+-----------+---------------+---------
1 | 1 | | 12 | 1 | $100.00
2 | 1 | | 12 | 2 | $140.00
3 | 1 | | 6 | 1 | $120.00
4 | 6 | 2 | 1 | | $80.00
5 | 12 | 1 | 1 | | $20.00
6 | 12 | 2 | 1 | | $40.00
(6 rows)
Получаем из журнала проводок главную книгу и сохраняем ее в таблице general_ledger:
DROP TABLE IF EXISTS general_ledger;
SELECT id AS gj_id,
debit_id AS acc_id,
debit_sub_id AS acc_sub_id,
credit_id AS cor_id,
credit_sub_id AS cor_sub_id,
amount AS debit_amount,
( 0.00 ) :: money AS credit_amount
INTO general_ledger
FROM general_journal
UNION
SELECT id AS gj_id,
credit_id AS acc_id,
credit_sub_id AS acc_sub_id,
debit_id AS cor_id,
debit_sub_id AS cor_sub_id,
( 0.00 ) :: money AS debit_amount,
amount AS credit_amount
FROM general_journal
ORDER BY gj_id;
DROP TABLE
SELECT 12
Выводим содержимое главной книги:
SELECT *
FROM general_ledger
ORDER BY gj_id;
gj_id | acc_id | acc_sub_id | cor_id | cor_sub_id | debit_amount | credit_amount
-------+--------+------------+--------+------------+--------------+---------------
1 | 1 | | 12 | 1 | $100.00 | $0.00
1 | 12 | 1 | 1 | | $0.00 | $100.00
2 | 1 | | 12 | 2 | $140.00 | $0.00
2 | 12 | 2 | 1 | | $0.00 | $140.00
3 | 1 | | 6 | 1 | $120.00 | $0.00
3 | 6 | 1 | 1 | | $0.00 | $120.00
4 | 1 | | 6 | 2 | $0.00 | $80.00
4 | 6 | 2 | 1 | | $80.00 | $0.00
5 | 1 | | 12 | 1 | $0.00 | $20.00
5 | 12 | 1 | 1 | | $20.00 | $0.00
6 | 1 | | 12 | 2 | $0.00 | $40.00
6 | 12 | 2 | 1 | | $40.00 | $0.00
(12 rows)
Обороты по синтетическим счетам:
SELECT acc_id,
sum(debit_amount) AS debit_turnout,
sum(credit_amount) AS credit_turnout
FROM general_ledger
GROUP BY acc_id
ORDER BY acc_id;
acc_id | debit_turnout | credit_turnout
--------+---------------+----------------
1 | $360.00 | $140.00
6 | $80.00 | $120.00
12 | $60.00 | $240.00
(3 rows)
Обороты по субсчетам:
SELECT acc_id,
acc_sub_id,
sum(debit_amount) AS debit_turnout,
sum(credit_amount) AS credit_turnout
FROM general_ledger
GROUP BY acc_id, acc_sub_id
ORDER BY acc_id, acc_sub_id;
acc_id | acc_sub_id | debit_turnout | credit_turnout
--------+------------+---------------+----------------
1 | | $360.00 | $140.00
6 | 1 | $0.00 | $120.00
6 | 2 | $80.00 | $0.00
12 | 1 | $20.00 | $100.00
12 | 2 | $40.00 | $140.00
(5 rows)
Step 3. Счет материалы. Синтетический учет
Сейчас ненадолго вернемся на Землю, в Россию. Будем манипулировать объектами, которые можно увидеть и потрогать. Используем счет 10 "Материалы".
Файл step3.sql:
DROP TABLE IF EXISTS general_journal;
CREATE TABLE general_journal(
id serial,
debit_id smallint NOT NULL,
credit_id smallint NOT NULL,
amount money NOT NULL
);
Выполняем проводки с использованием счета 10:
INSERT INTO general_journal(debit_id, credit_id, amount)
VALUES (10, 60, 400.00),
(19, 60, 72.00),
(20, 10, 50.00);
INSERT 0 3
Первой проводкой получаем какие-то материалы от поставщика. Второй проводкой начисляем НДС.
Третьей проводкой отпускаем материалы в производство.
Выводим содержимое журнала:
SELECT *
FROM general_journal
ORDER BY id;
id | debit_id | credit_id | amount
----+----------+-----------+---------
1 | 10 | 60 | $400.00
2 | 19 | 60 | $72.00
3 | 20 | 10 | $50.00
(3 rows)
Получаем из журнала проводок главную книгу и сохраняем ее в таблице general_ledger:
DROP TABLE IF EXISTS general_ledger;
SELECT id AS gj_id,
debit_id AS account_id,
credit_id AS corresp_id,
amount AS debit_amount,
( 0.00 ) :: money AS credit_amount
INTO general_ledger
FROM general_journal
UNION
SELECT id AS gj_id,
credit_id AS account_id,
debit_id AS corresp_id,
( 0.00 ) :: money AS debit_amount,
amount AS credit_amount
FROM general_journal
ORDER BY gj_id;
DROP TABLE
SELECT 6
Выводим содержимое главной книги:
SELECT *
FROM general_ledger
ORDER BY gj_id;
gj_id | account_id | corresp_id | debit_amount | credit_amount
-------+------------+------------+--------------+---------------
1 | 10 | 60 | $400.00 | $0.00
1 | 60 | 10 | $0.00 | $400.00
2 | 19 | 60 | $72.00 | $0.00
2 | 60 | 19 | $0.00 | $72.00
3 | 10 | 20 | $0.00 | $50.00
3 | 20 | 10 | $50.00 | $0.00
(6 rows)
Обороты по счетам:
SELECT account_id,
sum(debit_amount) AS debit_turnout,
sum(credit_amount) AS credit_turnout
FROM general_ledger
GROUP BY account_id
ORDER BY account_id;
account_id | debit_turnout | credit_turnout
------------+---------------+----------------
10 | $400.00 | $50.00
19 | $72.00 | $0.00
20 | $50.00 | $0.00
60 | $0.00 | $472.00
(4 rows)
Первые две проводки имеют отношение к одному событию — приход материалов. Этому событию могут соответствовать документы: товарно-транспортная накаладная и счет-фактура. Третья проводка — передача материалов со склада в производство. Это событие тоже сопровождается документом, например, накладной.
Таким образом, в general_journal мы можем добавить поле для идентификатора события или операции. А в таблице событий (или операций) организовать связь один-ко-многим с таблицей документов.
Процесс является последовательностью операций. Одним из элементарных операций может быть добавление проводок в бухгалтерскую систему. Для описания процессов может быть добавлена таблица шаблонов процессов.
В России событие прихода материалов в шаблонах будет сопровождаться двумя проводками по счету 10 и 19, а на западе, вероятно, будет одна проводка.
Step 4. Счет материалы. Аналитический учет
Материалы могут храниться на складах. Добавим склады и используем субсчета.
Файл step4.sql:
DROP TABLE IF EXISTS general_journal;
CREATE TABLE general_journal(
id serial,
debit_id smallint NOT NULL,
debit_sub_id smallint,
debit_stock_id smallint,
credit_id smallint NOT NULL,
credit_sub_id smallint,
credit_stock_id smallint,
amount money NOT NULL
);
DROP TABLE IF EXISTS stock;
CREATE TABLE stock(
id serial,
name text
);
Выполняем проводки с использованием счета 10:
INSERT INTO general_journal(debit_id, debit_sub_id, debit_stock_id, credit_id, credit_sub_id, credit_stock_id, amount)
VALUES (10, 1, 1, 60, 1, NULL, 100.00),
(10, 1, 2, 60, 1, NULL, 200.00),
(10, 2, 1, 60, 1, NULL, 100.00),
(19, 3, NULL, 60, 1, NULL, 72.00),
(20, 3, NULL, 10, 1, 1, 50.00);
INSERT 0 5
Выводим содержимое журнала:
SELECT *
FROM general_journal
ORDER BY id;
id | debit_id | debit_sub_id | debit_stock_id | credit_id | credit_sub_id | credit_stock_id | amount
----+----------+--------------+----------------+-----------+---------------+-----------------+---------
1 | 10 | 1 | 1 | 60 | 1 | | $100.00
2 | 10 | 1 | 2 | 60 | 1 | | $200.00
3 | 10 | 2 | 1 | 60 | 1 | | $100.00
4 | 19 | 3 | | 60 | 1 | | $72.00
5 | 20 | 3 | | 10 | 1 | 1 | $50.00
(5 rows)
Получаем из журнала проводок главную книгу и сохраняем ее в таблице general_ledger:
DROP TABLE IF EXISTS general_ledger;
SELECT id AS gj_id,
debit_id AS acc_id,
debit_sub_id AS acc_sub_id,
debit_stock_id AS acc_stock_id,
credit_id AS cor_id,
credit_sub_id AS cor_sub_id,
credit_stock_id AS cor_stock_id,
amount AS debit_amount,
( 0.00 ) :: money AS credit_amount
INTO general_ledger
FROM general_journal
UNION
SELECT id AS gj_id,
credit_id AS acc_id,
credit_sub_id AS acc_sub_id,
credit_stock_id AS acc_stock_id,
debit_id AS cor_id,
debit_sub_id AS cor_sub_id,
debit_stock_id AS cor_stock_id,
( 0.00 ) :: money AS debit_amount,
amount AS credit_amount
FROM general_journal
ORDER BY gj_id;
DROP TABLE
SELECT 10
Выводим содержимое главной книги:
SELECT *
FROM general_ledger
ORDER BY gj_id;
gj_id | acc_id | acc_sub_id | acc_stock_id | cor_id | cor_sub_id | cor_stock_id | debit_amount | credit_amount
-------+--------+------------+--------------+--------+------------+--------------+--------------+---------------
1 | 10 | 1 | 1 | 60 | 1 | | $100.00 | $0.00
1 | 60 | 1 | | 10 | 1 | 1 | $0.00 | $100.00
2 | 10 | 1 | 2 | 60 | 1 | | $200.00 | $0.00
2 | 60 | 1 | | 10 | 1 | 2 | $0.00 | $200.00
3 | 10 | 2 | 1 | 60 | 1 | | $100.00 | $0.00
3 | 60 | 1 | | 10 | 2 | 1 | $0.00 | $100.00
4 | 19 | 3 | | 60 | 1 | | $72.00 | $0.00
4 | 60 | 1 | | 19 | 3 | | $0.00 | $72.00
5 | 10 | 1 | 1 | 20 | 3 | | $0.00 | $50.00
5 | 20 | 3 | | 10 | 1 | 1 | $50.00 | $0.00
(10 rows)
Обороты по счетам:
SELECT acc_id,
sum(debit_amount) AS debit_turnout,
sum(credit_amount) AS credit_turnout
FROM general_ledger
GROUP BY acc_id
ORDER BY acc_id;
acc_id | debit_turnout | credit_turnout
--------+---------------+----------------
10 | $400.00 | $50.00
19 | $72.00 | $0.00
20 | $50.00 | $0.00
60 | $0.00 | $472.00
(4 rows)
Обороты по субсчетам:
SELECT acc_id,
acc_sub_id,
sum(debit_amount) AS debit_turnout,
sum(credit_amount) AS credit_turnout
FROM general_ledger
GROUP BY acc_id, acc_sub_id
ORDER BY acc_id, acc_sub_id;
acc_id | acc_sub_id | debit_turnout | credit_turnout
--------+------------+---------------+----------------
10 | 1 | $300.00 | $50.00
10 | 2 | $100.00 | $0.00
19 | 3 | $72.00 | $0.00
20 | 3 | $50.00 | $0.00
60 | 1 | $0.00 | $472.00
(5 rows)
Обороты по субсчетам и складам:
SELECT acc_id,
acc_sub_id,
acc_stock_id,
sum(debit_amount) AS debit_turnout,
sum(credit_amount) AS credit_turnout
FROM general_ledger
GROUP BY acc_id, acc_sub_id, acc_stock_id
ORDER BY acc_id, acc_sub_id, acc_stock_id;
acc_id | acc_sub_id | acc_stock_id | debit_turnout | credit_turnout
--------+------------+--------------+---------------+----------------
10 | 1 | 1 | $100.00 | $50.00
10 | 1 | 2 | $200.00 | $0.00
10 | 2 | 1 | $100.00 | $0.00
19 | 3 | | $72.00 | $0.00
20 | 3 | | $50.00 | $0.00
60 | 1 | | $0.00 | $472.00
(6 rows)
Обороты только по счету 10 и складам:
SELECT acc_stock_id,
sum(debit_amount) AS debit_turnout,
sum(credit_amount) AS credit_turnout
FROM general_ledger
WHERE acc_id = 10
GROUP BY acc_stock_id
ORDER BY acc_stock_id;
acc_stock_id | debit_turnout | credit_turnout
--------------+---------------+----------------
1 | $200.00 | $50.00
2 | $200.00 | $0.00
(2 rows)
Посмотрим на полученный журнал general_journal. Здесь, как видим, для счетов 19, 20 и 60 не нужны идентификаторы складов. Для счета 10 идентификатор склада по дебету нужен только для проводок по этому счету по дебету, и, наоборот, идентификатор склада по кредиту нужен для проводок по этому счету по кредиту.
Таким образом, мы можем попробовать создать для каждого счета собственный журнал со своим набором полей, то есть с собственной структурой. Попробуем это сделать на следующих днях.
Мы можем добавить поле для количественного учета материалов, которое будет не нужно только для 19 счета. Мы можем добавить для разных счетов другие нужные поля. С добавлением полей будет увеличиваться количество возможных отчетов.
Step 5. Занимательное
- Для описания процессов может быть разработана экспертная система. Содержимое главы 10 Knowledge Representation книги Artificial Intelligence. A Modern Approach может быть интересным. Тем более что при разработке плана счетов, при добавлении субсчетов и полей для аналитики затрагиваются темы классификации и онтологии.
Какой-то базовый контент этой экспертной системы будет разработан нами. Например, какой-то базовый контент систем учета для отдельных стран. Остальной контент для разных отраслей и конкретных производств может быть разработан на местах. Разработка и внедрение такого контента вполне может потянуть на суммы озвученные в конкурсной документации РФРИТ https://ит-гранты.рф/2. - Сейчас рассматриваю следующий стэк технологий: PostgreSQL, SQLAlchemy, FastAPI, Svelte/Material.
- Деплой на десктопы будет выполняться с помощью Ansible.
- Я так вижу здесь поддержка маркдауна скоро закончится и будет только визивиг редактор. Смотрите все остальные статьи на гитхабе https://github.com/nomhoi/empire-erp.
Комментарии (44)
Voila2000
04.09.2021 12:00Добавьте пожалуйста ссылку в начале статьи на предыдущие статьи цикла, а то непонятно зачем и почему. За статью спасибо, читаю с интересом.
Продолжение цикла "Занимательная бухгалтерия".
caballero
04.09.2021 19:30Как по мне сумму по проводкам лучше писать в одно поле с плюсом по дебету и с минусом по кредиту. И меньше места займет и проше рас считывать обороты и остатки достоточно просто просуммировать. И не надо проверять что на выхеде в деьете или кредите спмо поеажет по знаку
Naf2000
04.09.2021 20:36В таблице проводок сумма вообще одна, потому что там два поля счёт дебета и счёт кредита
Но стоит отличать ситуации
Дт 10 Кт 60 сумма -125
И
Дт 60 Кт 10 сумма 125
caballero
04.09.2021 20:45+2ну вот по минусу и видно что там кредитовое сальдо или оборот
а если надо развернутое сальдо ничего не мешает в запросе написать when case и получить отдельно по плюсам и минусам
я в своей системе так сделал. причем остатки считаются полным пересчетом а не с промежуточными остатками как в той же одноце
Очегнь удобно считать за любой переио перепроводить документы задним числом и так далее. конечно больше нагрузка на сервер но железо ща стоит намного дешевле труда програмиста
shavluk
04.09.2021 22:21У меня для структура таблицы для проводок
Id_счета, Знак, Сумма, id_контрагента, id_проводки
id_проводки связывает 2 строки в одну (и соответствует понятию "двойная запись")
id_контрагента - это как организации так и свои склады/кассы/расч. счета
Знак позволяет различить ситуацию Дт 10 Кт 60 сумма -125 и Дт 60 Кт 10 сумма 125
Такая структура большинство отчетов делает тривиальными
Любой отчет считается полным пересчетом, но по закрытию периода я в таблицу проводок вношу остатки: половина проводки, с признаком остаток.
В дальнейшем остаток считается от ближайшего закрытия.
Когда-то здесь описывал упрощенный вариант моего учета https://www.sql.ru/forum/1321016/zhurnal-dvizheniya-deneg
Naf2000
04.09.2021 23:15Так-то проводки, а то сальдо
Но в таком случае как отличить обороты? это увеличениеоборота по кредиту или уменьшение по дебету?
Допускаю, что кому-то это не нужно
caballero
05.09.2021 00:28+1сальдо - результат проводок как и оборот. просчитайте проводки на дату с начала учета то есть с самой первой и получите сальдо.
отличить по знаку как я уже писал - получить равернутое сальдо не проблема хотя это нужно в редких случаях когда применяется бухгалтерское сторно
shavluk
05.09.2021 13:06Как выше уже сказали сальдо и обороты это результат суммирования проводок за весь период.
nomhoi Автор
05.09.2021 05:10Я так сделал в позапрошлой статье. В комментариях указали на ошибку в оборотах при проведении сторно: https://habr.com/ru/post/471304/#comment_20748390
shavluk
05.09.2021 23:02Получаем из журнала проводок главную книгу и сохраняем ее
При использовании структуры таблиц, в котором проводка разбита на 2 строки получение оборотов + остатки на начало и на конец периода (сальдо) это запрос вида
select account_id,
sum(case when DATE >= '01.01.2021' and "Знак" = 1 then amount else 0) AS debit_turnout,
sum(case when DATE >= '01.01.2021' and "Знак" = -1 then amount else 0) AS credit_turnout,
sum("Знак" * amount) AS saldo,
sum(case when DATE < '01.01.2021' then "Знак"* amount) AS saldo_begin
from "Таблица_полупроводок"
group by account_id
Такой запрос гораздо лучше индексируется (по сравнению с запросом с UNION)
Про субсчета. Для каждого счета есть признак главный счет и соответственно группировать можно по главному счету.
Ваша структура легко приводится к такой путем добавлением триггера на general_journal (здесь разделять на 2 строки). Хотя конечно, с моей т.з. дублирование всех полей на счет, субсчет, склад для дебета и кредита себе идея.Naf2000
07.09.2021 11:41+1Что такое "Знак"?
shavluk
07.09.2021 16:14Поле признак, для дебета = "+1", для кредита = "-1".
Проводка разбивается на две строки: дебетная и кредитная. Поля в строке: счет, склад/организация, дата, сумма, знак, id_проводки (дополнительно можно примечание, id_документа, и т.д.)
id_проводки в вашем случае может соответствовать general_journal.id и нужно в дальнейшем для отображения проводки в "канонической форме" (должно быть уникальным для обеих строк)
Столбцы: дата, сумма, id_проводки у обеих строк одинаковые.
nomhoi Автор
10.09.2021 07:04дублирование всех полей на счет, субсчет, склад для дебета и кредита себе идея.
Я планирую рассмотреть использование вспомогательных журналов и/или книг для записи проводок. Для каждого счета можно сделать свой журнал со своим набором полей, которые нужны только для этого счета. Возможно, в этом случае дублирование уйдет.
С другой стороны, удобно обортную ведомость получать.
nomhoi Автор
12.09.2021 16:16Со вспомогательными журналами получается так:
https://github.com/nomhoi/empire-erp/tree/master/research/day4shavluk
12.09.2021 22:05Мне кажется что разделение проводок по разным таблицам-журналам это неправильный подход. Вы привязываетесь к номерам журналов и конкретному плану счетов. Они имеют свойство меняться. Я не знаком с этими номерами в РФ, но мне кажется номер журнала можно хранить в проводке. И изменение страны для которой происходит учет не должно влиять на структуру таблиц.
Таблицу счетов можно сделать древовидной, а в таблице операций хранить только один счет, самого низкого уровня. И обороты по главному счету получать при помощи объединения с таблицей проводок.
У меня структура таблиц в бухгалтерии:
1. Документ (набор таблиц с датой, типом, контрагентами, списком товаров и т.д.). У меня 2 основные таблицы. Документ имеет 2 состояния: черновик и проведенный документ.
В момент проведения на основе настроек конкретного типа документа, вида товаров и т.д. формируются проводки.2. Проводки (или точнее полупроводки). При проведении документа формируется набор проводок, где каждая проводка это 2 строки и полями: id_документа, дата, счет, организация, сумма, знак (1=дебет, -1=кредит), примечание, id_операции (связь дебета с кредитом). Сюда же можно и воткнуть номер журнала.
3. Движение товаров. При перемещении товаров у меня формируется таблица похожая на проводки, но немного проще: id_документа, дата, id_товара, id_склада, кол-во, сумма, знак (1=приход, -1=расход), id_партии (для партионного учета)
4. Справочники (счетов, организаций, товаров и т.д.)
Все остальное: обороты, остатки, движение получается довольно простыми запросами.nomhoi Автор
13.09.2021 07:15Мне кажется что разделение проводок по разным таблицам-журналам это неправильный подход.
Разные журналы могут иметь свой набор полей, который нужен только им.
Вы привязываетесь к номерам журналов и конкретному плану счетов. Они имеют свойство меняться. Я не знаком с этими номерами в РФ, но мне кажется номер журнала можно хранить в проводке. И изменение страны для которой происходит учет не должно влиять на структуру таблиц.
Да, конечно, я об этом думаю. Пока рассмотрел частный случай, для России.
Таблицу счетов можно сделать древовидной, а в таблице операций хранить только один счет, самого низкого уровня. И обороты по главному счету получать при помощи объединения с таблицей проводок.
Мне знакома такая терминология: синтетические счета - счета первого порядка, субсчета - счета второго порядка, субсчета могут быть разделены на свои собственные субсчета - счета третьего порядка и следующие порядков. Главный счет, я так понимаю - синтетический счет, а счета самого низкого уровня - это субсчета самого глубокого порядка? Правильно понимаю?
А с производительностью как в этом случае? Для получения регистров обычная группировка с агрегацией, мне кажется, быстрее будет работать.
У меня структура таблиц в бухгалтерии:
Как-нибудь, дело дойдет, нарисую uml диаграммы.
shavluk
13.09.2021 11:17Да, в проводке хранится только самьій "низкий" уровень счета.
Не думаю что некоторая избьіточность полей в таблице журналов является веской причиной для отдельньіх таблиц.С производительностью проблем нет. Для примера, у меня основная таблица проводок >15М записей. Дополнительно раз в месяц происходит закрьітие периода (можно и чаще), во время которого собираются промежуточньіе итоги (они тоже хранятся в таблице проводок). Большинство отчетов получается за время не более 2х секунд.
Я ранее здесь давал ссьілку на SQLru, там описана структура таблиц детальнее
nomhoi Автор
19.09.2021 11:29Да, в проводке хранится только самьій "низкий" уровень счета.
В проводках всегда должен указываться самый крайний субсчет, т.е. самого "низкого" порядка.
Если в нашем случае мы для субсчета укажем NOT NULL,acc_sub_id smallint NOT NULL,
то будет невозможно создать проводку без указания самого крайнего субсчета. СУБД сама будет нас защищать от создания ошибочной проводки, будет поднимать исключение.
Для разных счетов может быть разное количество порядков, поэтому придется создавать отдельные журналы или книги.
В вашем случае как выполняется защита от создания подобных неправильных проводок? Нужно всегда помнить, является ли счет крайним или нет?shavluk
19.09.2021 14:51У меня на этот случай нет никаких ограничений на уровне СУБД. Пользователь сам решает какой счет использовать и с каким уровнем вложенности работать.
Для запрета на уровне СУБД в справочнике счетов достаточно ввести поле (самый низкий уровень), и разрешать использовать только такие поля. Поле обновлять автоматически при редактировании справочника счетов.nomhoi Автор
19.09.2021 15:21Пользователь сам решает какой счет использовать и с каким уровнем вложенности работать.
В проводках нельзя использовать счет, у которого есть потомки. Обязательно нужно указывать самый крайний счет. В вашей юрисдикции существует такое правило?
nomhoi Автор
19.09.2021 11:41Не думаю что некоторая избьіточность полей в таблице журналов является веской причиной для отдельньіх таблиц.
Для многих генераторов отчетов, если нужно получить отчет с агрегацией по группам, нужно подавать датасет примерно в таком виде:
acc_id | acc_sub_id | stock_id | debit_turnout | credit_turnout --------+------------+----------+---------------+---------------- 10 | 1 | 1 | $100.00 | $50.00 10 | 1 | 2 | $200.00 | $0.00 10 | 2 | 1 | $100.00 | $0.00
в данном случае генератор отчетов самостоятельно вычислит итоговые суммы по складам, субсчетам и счетам, а также итоговую сумму в целом по отчету.
В вашем случае как выглядит запрос получения подобного датасета, если мы имеем несколько порядков субсчетов?shavluk
19.09.2021 14:55Я не совсем понимаю вопрос.
Вам непонятно как сделать сумму по "дереву" счетов?
В древовидном справочнике счетов есть поле "Родитель".
Можно добавить еще одно "Родитель для группировки". Здесь и будет самьій верхний уровень. Зачем добавлять это поле в проводку я не понимаю.select СЧЕТ.ГЛАВНЫЙ_СЧЕТ,
sum(case when ПРОВОДКИ.ЗНАК = 1, ПРОВОДКИ.СУММА else 0 end) debit_turnout,
sum(case when ПРОВОДКИ.ЗНАК = -1, ПРОВОДКИ.СУММА else 0 end) credit_turnout,
from ПРОВОДКИ
join СЧЕТ on СЧЕТ.id = ПРОВОДКИ.счета
group by СЧЕТ.ГЛАВНЫЙ_СЧЕТnomhoi Автор
19.09.2021 15:11У вас есть такие отчеты, в которых выполняется агрегация по группам?
Например, в этом отчете выполняется группировка по полю Cust ID.
И здесь мы видим суммы для каждой такой группы.
Таких групп в одном отчете может быть несколько. В вашем случае как это можно выполнить? Т.е. как можно подготовить датасет для такого отчета?shavluk
19.09.2021 15:28Да, конечно есть.
select o.CustId, o.AccountId, min(c.adress), sum(o.summa * o.sign) from oper o
left join customers c on c.id = o.CustId
group by o.CustId, o.AccountId
Итоги считаются по паре CustID, AccountId. А дальнейшие подитоги это не дело СУБД, легко решается на уровне генератора отчетов.
Но можно и извратиться для получения этих данных одним запросом.
но тут надо включать тяжелую артиллерию SQL - "WITH RECURSIVE"nomhoi Автор
19.09.2021 15:42Понятно. Про промежуточные итоги я написал выше, это выполняется генераторами. А join не увеличивает время выполнения запросов?
shavluk
19.09.2021 16:55Для ускорения можно сначала сгруппировать, а потом уже на готовые данные делать join.
Обычно такой запрос тоже делается не по всем счетам, а только по какой-то группе. Для этого я объединяю с таблицей групп счетов.
Структура такой таблицы простая (id_группы, id_счета)
nomhoi Автор
19.09.2021 17:05Ну, в простом случае можно сразу выполнить группировку без джойна и получить нужный датасет:
GROUP_BY parent_id, acc_sub_id
А что, если нужен отчет с несколькими уровнями группировок больше двух?
shavluk
19.09.2021 18:58А зачем боятся джойна? Хранение в справочнике операций счет, главный в группировке решает все проблемы
nomhoi Автор
19.09.2021 16:23Добавил реализацию со вспомогательными книгами:
shavluk
19.09.2021 16:57Все же "книга" это свойство "проводки". Наравне со счетом, организацией, суммой.
nomhoi Автор
19.09.2021 17:00Все же "книга" это свойство "проводки".
Не совсем понял эту мысль. Можно подробнее?
Я опираюсь на такие определения: https://en.wikipedia.org/wiki/General_ledger и https://en.wikipedia.org/wiki/Ledger
shavluk
19.09.2021 18:25В таблицу проводок добавляется поле LedgerID и главная книга это простой отчет
с остатком на начало, оборотами и остатком на конецselect LedgerID, AccountId,
sum(case when sign = 1 and date_oper < '01.01.2021' then summa else 0 end) debet_begin,
sum(case when sign = -1 and date_oper < '01.01.2021 then summa else 0 end) kredit_begin,
sum(case date_oper < '01.01.2021 then sign * summa else 0 end) summa_begin,
sum(case when sign = 1 and date_oper >= '01.01.2021' then summa else 0 end) debet_turnout,
sum(case when sign = -1 and date_oper >= '01.01.2021' then summa else 0 end) kredit_turnout,
sum(case date_oper >= '01.01.2021' then sign * summa else 0 end) sum_turnout,
sum(case when sign = 1 then summa else 0 end) debet_end,
sum(case when sign = -1 then summa else 0 end) kredit_end, sum(sign * summa) summa_end
from oper
where date_oper <= '31.01.2021'
group by LedgerID, AccountId
shavluk
19.09.2021 18:34Еще раз про структуру таблицы "полупроводок" пусть будет OPER
Дата
ID счета
ID контрагента (склад/сотруник/покупатель/поставщик и т.д.)
Знак (1 для дебета, -1 для кредита)
Сумма
ID книги (в какой должна фигурировать проводка)
-
Пара (поле связывающее строку дебет со строкой кредит, в моем случае это поле одинаковое для двух строк)
Далее необязательные поля, но важные ID документа (у одного документа есть набор проводок)
Примечание (описание проводки)
У меня еще есть дополнительные поля, но для объяснения они пока не важны
Получение одной проводки из такой таблицы получается при помощи joinselect d.*, k.* from oper d
join oper k on k.para = d.para and k.sign = -1
where d.sign = 1
shavluk
19.09.2021 18:54При такой структуре есть необходимость ускорять расчеты на начало периода. Т.к. весь учет происходит от начала истории. Есть разные способы. Я для себя изобрел следующий:
В таблицу OPER добавляется поле "ПРИЗНАК ОСТАТКА" у меня OST, для обычной проводки в этом поле 0.
Периодически (раз в день/неделю/месяц/квартал/год) происходит закрытие периода и в таблицу OPER добавляются текущие остатки по счетам/организациям. Но не виде проводки, а одиночные строки с полем OST = 1. И в отдельную таблицу вносится дата закрытия (у меня PERIOD)
В дальнейшем для получения отчетов с остатками на начало по таблице PERIOD находим ближайшую дату снизу к дате начала периода.
И отчет модернизируется доwhere (o.ost = 1 and o.date_oper = 'дата закрытия') or (o.ost = 0 and o.date_oper between 'дата закрытия' + 1 and 'конечная дата отчета')
Такая схема работает у меня очень давно и проблем с производительностью нет никаких
Naf2000
Спасибо, что показали свое видение внутреннего устройства бух. учета. Есть несколько вопросов и замечаний:
Непонятно, что из этого материальные таблицы, а что представления и запросы.
Как-то не отражено соответствие и субсчетов, а если субсчета у субсчетов? Из активность/пассивность где отражена? Намек на таблицу плана счетов.
Где сальдо? При этом у активных счетов - оно только по дебету (может отрицательным быть и значит что-то не так), у пассивных - по кредиту, у активно-пассивных оно развернутое.
Аналитика только одна. А если две? Например 10 счет по складам и материалам.
Как обеспечить ссылочную целостность аналитического учета, если "10" это склады и материалы, а "60" это поставщики?
Не представлен количественный и валютный учет.
Ну и конечно проводки имеют дату отражения в учете.
Naf2000
Извиняюсь, я с остальным Вашим творчеством еще не ознакомился.
nomhoi Автор
По всем вопросам - я пока провожу исследования, изучаю эту область. На некоторые вопросы ответы в прошлой статье. Рассмотрение аналитического учета только началось.
Вообще, я планирую использовать ORM. Нужна поддержка миграций и кастомизации моделей. С ORM-ом это вроде делается проще.
Счет в одном поле, субсчет в другом поле. Субсчета других порядков можно выполнить аналогично, в отдельных полях.
Можно добавить поле - идентификатор операции.
shavluk
Вариант с полями под субсчета мне кажется дорогой не туда. Иногда счет сначала ввели как основной, а потом его надо сделать субсчетом и наоборот.
nomhoi Автор
А можно пример из реальной практики? Какие счета и субсчета использовались?
Много ли найдется организаций, которым пришлось это выполнить? Можно же просто написать скрипт, который все это исправит.
Naf2000
Как это поможет с ссылочной целостностью?
nomhoi Автор
Прошу прощения, немного не в тему ответил. Сейчас, пока собирается информация какие поля нужны и какая структура данных нужна, наверное, преждевременно декларировать внешние ключи и т.д. Когда все более менее определится, то можно будет все и определить. Возможно, уже при создании моделей на SQLAlchemy.
Идентификатор операции - для того, чтобы связать все проводки имеющие отношение к одной операции. В данном случае, в синтетическом учете будет одна проводка, а в аналитическом учете три проводки получается: материал разложился на 2 субсчета и 2 склада. Кроме того, планируется рассмотреть использование вспомогательных журналов и книг.
nomhoi Автор
Добавляем поле для материалов.