Содержание цикла статей: 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. Занимательное


  1. Для описания процессов может быть разработана экспертная система. Содержимое главы 10 Knowledge Representation книги Artificial Intelligence. A Modern Approach может быть интересным. Тем более что при разработке плана счетов, при добавлении субсчетов и полей для аналитики затрагиваются темы классификации и онтологии.
    Какой-то базовый контент этой экспертной системы будет разработан нами. Например, какой-то базовый контент систем учета для отдельных стран. Остальной контент для разных отраслей и конкретных производств может быть разработан на местах. Разработка и внедрение такого контента вполне может потянуть на суммы озвученные в конкурсной документации РФРИТ https://ит-гранты.рф/2.
  2. Сейчас рассматриваю следующий стэк технологий: PostgreSQL, SQLAlchemy, FastAPI, Svelte/Material.
  3. Деплой на десктопы будет выполняться с помощью Ansible.
  4. Я так вижу здесь поддержка маркдауна скоро закончится и будет только визивиг редактор. Смотрите все остальные статьи на гитхабе https://github.com/nomhoi/empire-erp.

Комментарии (44)


  1. Naf2000
    04.09.2021 11:34
    +1

    Спасибо, что показали свое видение внутреннего устройства бух. учета. Есть несколько вопросов и замечаний:

    Непонятно, что из этого материальные таблицы, а что представления и запросы.

    Как-то не отражено соответствие и субсчетов, а если субсчета у субсчетов? Из активность/пассивность где отражена? Намек на таблицу плана счетов.

    Где сальдо? При этом у активных счетов - оно только по дебету (может отрицательным быть и значит что-то не так), у пассивных - по кредиту, у активно-пассивных оно развернутое.

    Аналитика только одна. А если две? Например 10 счет по складам и материалам.

    Как обеспечить ссылочную целостность аналитического учета, если "10" это склады и материалы, а "60" это поставщики?

    Не представлен количественный и валютный учет.

    Ну и конечно проводки имеют дату отражения в учете.


    1. Naf2000
      04.09.2021 11:36

      Извиняюсь, я с остальным Вашим творчеством еще не ознакомился.


    1. nomhoi Автор
      05.09.2021 05:43

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

      Непонятно, что из этого материальные таблицы, а что представления и запросы.

      Вообще, я планирую использовать ORM. Нужна поддержка миграций и кастомизации моделей. С ORM-ом это вроде делается проще.

      Как-то не отражено соответствие и субсчетов, а если субсчета у субсчетов?

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

      Как обеспечить ссылочную целостность аналитического учета, если "10" это склады и материалы, а "60" это поставщики?

      Можно добавить поле - идентификатор операции.


      1. shavluk
        05.09.2021 13:05

        Вариант с полями под субсчета мне кажется дорогой не туда. Иногда счет сначала ввели как основной, а потом его надо сделать субсчетом и наоборот.


        1. nomhoi Автор
          10.09.2021 06:54

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


      1. Naf2000
        05.09.2021 16:38

        Можно добавить поле - идентификатор операции.

        Как это поможет с ссылочной целостностью?


        1. nomhoi Автор
          10.09.2021 07:01

          Прошу прощения, немного не в тему ответил. Сейчас, пока собирается информация какие поля нужны и какая структура данных нужна, наверное, преждевременно декларировать внешние ключи и т.д. Когда все более менее определится, то можно будет все и определить. Возможно, уже при создании моделей на SQLAlchemy.

          Идентификатор операции - для того, чтобы связать все проводки имеющие отношение к одной операции. В данном случае, в синтетическом учете будет одна проводка, а в аналитическом учете три проводки получается: материал разложился на 2 субсчета и 2 склада. Кроме того, планируется рассмотреть использование вспомогательных журналов и книг.


    1. nomhoi Автор
      05.09.2021 06:37

      Аналитика только одна. А если две? Например 10 счет по складам и материалам.

      Добавляем поле для материалов.


  1. Voila2000
    04.09.2021 12:00

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


    1. nomhoi Автор
      05.09.2021 09:52

      Добавил ссылку на содержание цикла статей.


  1. caballero
    04.09.2021 19:30

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


    1. Naf2000
      04.09.2021 20:36

      В таблице проводок сумма вообще одна, потому что там два поля счёт дебета и счёт кредита

      Но стоит отличать ситуации

      Дт 10 Кт 60 сумма -125

      И

      Дт 60 Кт 10 сумма 125


      1. caballero
        04.09.2021 20:45
        +2

        ну вот по минусу и видно что там кредитовое сальдо или оборот

        а если надо развернутое сальдо ничего не мешает в запросе написать when case и получить отдельно по плюсам и минусам

        я в своей системе так сделал. причем остатки считаются полным пересчетом а не с промежуточными остатками как в той же одноце

        Очегнь удобно считать за любой переио перепроводить документы задним числом и так далее. конечно больше нагрузка на сервер но железо ща стоит намного дешевле труда програмиста


        1. 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


        1. Naf2000
          04.09.2021 23:15

          Так-то проводки, а то сальдо

          Но в таком случае как отличить обороты? это увеличениеоборота по кредиту или уменьшение по дебету?

          Допускаю, что кому-то это не нужно


          1. caballero
            05.09.2021 00:28
            +1

            сальдо - результат проводок как и оборот. просчитайте проводки на дату с начала учета то есть с самой первой и получите сальдо.

            отличить по знаку как я уже писал - получить равернутое сальдо не проблема хотя это нужно в редких случаях когда применяется бухгалтерское сторно


          1. shavluk
            05.09.2021 13:06

            Как выше уже сказали сальдо и обороты это результат суммирования проводок за весь период.


    1. nomhoi Автор
      05.09.2021 05:10

      Я так сделал в позапрошлой статье. В комментариях указали на ошибку в оборотах при проведении сторно: https://habr.com/ru/post/471304/#comment_20748390


  1. 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 строки). Хотя конечно, с моей т.з. дублирование всех полей на счет, субсчет, склад для дебета и кредита себе идея.


    1. Naf2000
      07.09.2021 11:41
      +1

      Что такое "Знак"?


      1. shavluk
        07.09.2021 16:14

        Поле признак, для дебета = "+1", для кредита = "-1".

        Проводка разбивается на две строки: дебетная и кредитная. Поля в строке: счет, склад/организация, дата, сумма, знак, id_проводки (дополнительно можно примечание, id_документа, и т.д.)
        id_проводки в вашем случае может соответствовать general_journal.id и нужно в дальнейшем для отображения проводки в "канонической форме" (должно быть уникальным для обеих строк)
        Столбцы: дата, сумма, id_проводки у обеих строк одинаковые.


    1. nomhoi Автор
      10.09.2021 07:04

       дублирование всех полей на счет, субсчет, склад для дебета и кредита себе идея.

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

      С другой стороны, удобно обортную ведомость получать.


    1. nomhoi Автор
      12.09.2021 16:16

      Со вспомогательными журналами получается так:
      https://github.com/nomhoi/empire-erp/tree/master/research/day4


      1. shavluk
        12.09.2021 22:05

        Мне кажется что разделение проводок по разным таблицам-журналам это неправильный подход. Вы привязываетесь к номерам журналов и конкретному плану счетов. Они имеют свойство меняться. Я не знаком с этими номерами в РФ, но мне кажется номер журнала можно хранить в проводке. И изменение страны для которой происходит учет не должно влиять на структуру таблиц.

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


        У меня структура таблиц в бухгалтерии:
        1. Документ (набор таблиц с датой, типом, контрагентами, списком товаров и т.д.). У меня 2 основные таблицы. Документ имеет 2 состояния: черновик и проведенный документ.
        В момент проведения на основе настроек конкретного типа документа, вида товаров и т.д. формируются проводки.

        2. Проводки (или точнее полупроводки). При проведении документа формируется набор проводок, где каждая проводка это 2 строки и полями: id_документа, дата, счет, организация, сумма, знак (1=дебет, -1=кредит), примечание, id_операции (связь дебета с кредитом). Сюда же можно и воткнуть номер журнала.

        3. Движение товаров. При перемещении товаров у меня формируется таблица похожая на проводки, но немного проще: id_документа, дата, id_товара, id_склада, кол-во, сумма, знак (1=приход, -1=расход), id_партии (для партионного учета)


        4. Справочники (счетов, организаций, товаров и т.д.)

        Все остальное: обороты, остатки, движение получается довольно простыми запросами.


        1. nomhoi Автор
          13.09.2021 07:15

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

          Разные журналы могут иметь свой набор полей, который нужен только им.

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

          Да, конечно, я об этом думаю. Пока рассмотрел частный случай, для России.

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

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

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

          У меня структура таблиц в бухгалтерии:

          Как-нибудь, дело дойдет, нарисую uml диаграммы.


          1. shavluk
            13.09.2021 11:17

            Да, в проводке хранится только самьій "низкий" уровень счета.
            Не думаю что некоторая избьіточность полей в таблице журналов является веской причиной для отдельньіх таблиц.

            С производительностью проблем нет. Для примера, у меня основная таблица проводок >15М записей. Дополнительно раз в месяц происходит закрьітие периода (можно и чаще), во время которого собираются промежуточньіе итоги (они тоже хранятся в таблице проводок). Большинство отчетов получается за время не более 2х секунд.

            Я ранее здесь давал ссьілку на SQLru, там описана структура таблиц детальнее


            1. nomhoi Автор
              13.09.2021 13:37

              Спасибо, я посмотрю.


            1. nomhoi Автор
              19.09.2021 11:29

              Да, в проводке хранится только самьій "низкий" уровень счета.

              В проводках всегда должен указываться самый крайний субсчет, т.е. самого "низкого" порядка.
              Если в нашем случае мы для субсчета укажем NOT NULL,

              acc_sub_id  smallint NOT NULL,    

              то будет невозможно создать проводку без указания самого крайнего субсчета. СУБД сама будет нас защищать от создания ошибочной проводки, будет поднимать исключение.
              Для разных счетов может быть разное количество порядков, поэтому придется создавать отдельные журналы или книги.
              В вашем случае как выполняется защита от создания подобных неправильных проводок? Нужно всегда помнить, является ли счет крайним или нет?


              1. shavluk
                19.09.2021 14:51

                У меня на этот случай нет никаких ограничений на уровне СУБД. Пользователь сам решает какой счет использовать и с каким уровнем вложенности работать.
                Для запрета на уровне СУБД в справочнике счетов достаточно ввести поле (самый низкий уровень), и разрешать использовать только такие поля. Поле обновлять автоматически при редактировании справочника счетов.


                1. nomhoi Автор
                  19.09.2021 15:21

                  Пользователь сам решает какой счет использовать и с каким уровнем вложенности работать.

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


            1. 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

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


              1. 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 СЧЕТ.ГЛАВНЫЙ_СЧЕТ


                1. nomhoi Автор
                  19.09.2021 15:11

                  У вас есть такие отчеты, в которых выполняется агрегация по группам?

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

                  Таких групп в одном отчете может быть несколько. В вашем случае как это можно выполнить? Т.е. как можно подготовить датасет для такого отчета?


                  1. 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"


                    1. nomhoi Автор
                      19.09.2021 15:42

                      Понятно. Про промежуточные итоги я написал выше, это выполняется генераторами. А join не увеличивает время выполнения запросов?


                      1. shavluk
                        19.09.2021 16:55

                        Для ускорения можно сначала сгруппировать, а потом уже на готовые данные делать join.

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

                        Структура такой таблицы простая (id_группы, id_счета)


                      1. nomhoi Автор
                        19.09.2021 17:05

                        Ну, в простом случае можно сразу выполнить группировку без джойна и получить нужный датасет:
                        GROUP_BY parent_id, acc_sub_id
                        А что, если нужен отчет с несколькими уровнями группировок больше двух?


                      1. shavluk
                        19.09.2021 18:58

                        А зачем боятся джойна? Хранение в справочнике операций счет, главный в группировке решает все проблемы


  1. nomhoi Автор
    19.09.2021 16:23

    1. shavluk
      19.09.2021 16:57

      Все же "книга" это свойство "проводки". Наравне со счетом, организацией, суммой.


      1. nomhoi Автор
        19.09.2021 17:00

        Все же "книга" это свойство "проводки".

        Не совсем понял эту мысль. Можно подробнее?

        Я опираюсь на такие определения: https://en.wikipedia.org/wiki/General_ledger и https://en.wikipedia.org/wiki/Ledger


        1. 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


        1. shavluk
          19.09.2021 18:34

          Еще раз про структуру таблицы "полупроводок" пусть будет OPER

          1. Дата

          2. ID счета

          3. ID контрагента (склад/сотруник/покупатель/поставщик и т.д.)

          4. Знак (1 для дебета, -1 для кредита)

          5. Сумма

          6. ID книги (в какой должна фигурировать проводка)

          7. Пара (поле связывающее строку дебет со строкой кредит, в моем случае это поле одинаковое для двух строк)


            Далее необязательные поля, но важные

          8. ID документа (у одного документа есть набор проводок)

          9. Примечание (описание проводки)

          У меня еще есть дополнительные поля, но для объяснения они пока не важны


          Получение одной проводки из такой таблицы получается при помощи join
          select d.*, k.* from oper d
          join oper k on k.para = d.para and k.sign = -1
          where d.sign = 1


  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 'конечная дата отчета')

    Такая схема работает у меня очень давно и проблем с производительностью нет никаких