Empire ERP. Contabilidade divertida: contabilidade, contas, saldo

Neste artigo, tentaremos penetrar no coração da "empresa sangrenta" - contabilidade. Primeiro, faremos um estudo do razão geral, das contas e do balanço, identificaremos suas propriedades e algoritmos inerentes. Usamos a tecnologia Python e Test Driven Development. Aqui estaremos envolvidos na prototipagem; portanto, em vez do banco de dados, usaremos os contêineres básicos: listas, dicionários e tuplas. O projeto está sendo desenvolvido de acordo com os requisitos do projeto Empire ERP .


Condição da tarefa


Espaço ... Planeta da Império ... Um estado em todo o planeta. A população trabalha 2 horas em 2 semanas, após 2 anos para se aposentar. O plano de contas consiste em 12 posições. As contas 1 a 4 estão ativas, 5 a 8 são passivas ativas e 9 a 12 são passivas. Empresa Horns & Hooves. Todas as transações são realizadas em um período de relatório, no início do período não há saldos.


Configuração do projeto


Clonamos o projeto no github:


git clone https://github.com/nomhoi/empire-erp.git 

Estamos desenvolvendo no Python 3.7.4. Configure o ambiente virtual, ative-o e instale o pytest .


 pip install pytest 

1. Contabilidade


Vá para a pasta reaserch / day1 / step1 .


accounting.py :


 DEBIT = 0 CREDIT = 1 AMOUNT = 2 class GeneralLedger(list): def __str__(self): res = '\nGeneral ledger' for e in self: res += '\n {:2} {:2} {:8.2f}'.format(e[DEBIT], e[CREDIT], e[AMOUNT]) res += "\n----------------------" return res 

test_accounting.py :


 import pytest from accounting import * from decimal import * @pytest.fixture def ledger(): return GeneralLedger() @pytest.mark.parametrize('entries', [ [(1, 12, 100.00), (1, 11, 100.00)] ]) def test_ledger(ledger, entries): for entry in entries: ledger.append((entry[DEBIT], entry[CREDIT], Decimal(entry[AMOUNT]))) assert len(ledger) == 2 assert ledger[0][DEBIT] == 1 assert ledger[0][CREDIT] == 12 assert ledger[0][AMOUNT] == Decimal(100.00) assert ledger[1][DEBIT] == 1 assert ledger[1][CREDIT] == 11 assert ledger[1][AMOUNT] == Decimal(100.00) print(ledger) 

O livro principal, como vemos, é apresentado na forma de uma lista de registros. Cada entrada é projetada como uma tupla. Até o momento, para o registro da transação, usamos apenas os números de conta para débito e crédito e o valor da transação. Datas, descrições e outras informações ainda não são necessárias, iremos adicioná-las mais tarde.


A trava do razão e o teste parametrizado test_ledger foram criados no arquivo de teste. No parâmetro de teste de entradas , transferimos imediatamente toda a lista de transações. Para verificar, executamos o comando pytest -s -v no terminal. O teste deve passar e veremos no terminal toda a lista de transações armazenadas no Razão:


 General ledger 1 12 100.00 1 11 100.00 

2. Contas


Agora vamos adicionar suporte à fatura no projeto. Vá para a pasta day1 / step2 .


accounting.py :


 class GeneralLedger(list): def __init__(self, accounts=None): self.accounts = accounts def append(self, entry): if self.accounts is not None: self.accounts.append_entry(entry) super().append(entry) 

A classe GeneralLedger sobrecarregou o método append . Quando você adiciona uma transação a um livro, nós a adicionamos imediatamente às contas.


accounting.py :


 class Account: def __init__(self, id, begin=Decimal(0.00)): self.id = id self.begin = begin self.end = begin self.entries = [] def append(self, id, amount): self.entries.append((id, amount)) self.end += amount class Accounts(dict): def __init__(self): self.range = range(1, 13) for i in self.range: self[i] = Account(i) def append_entry(self, entry): self[entry[DEBIT]].append(entry[CREDIT], Decimal(entry[AMOUNT])) self[entry[CREDIT]].append(entry[DEBIT], Decimal(-entry[AMOUNT])) 

A classe Accounts foi criada como um dicionário. Nas chaves, o número da conta, nos valores, o conteúdo da conta, ou seja, uma instância da classe Account , que por sua vez contém os campos de saldo de abertura e fechamento e uma lista de transações relacionadas a esta conta. Observe que nesta lista os valores das transações de débito e crédito são armazenados em um campo, o valor do débito é positivo e o valor do empréstimo é negativo.


test_accounting.py :


 @pytest.fixture def accounts(): return Accounts() @pytest.fixture def ledger(accounts): return GeneralLedger(accounts) 

No arquivo de teste, adicionamos o bloqueio de contas e ajustamos o bloqueio do razão .


test_accounting.py :


 @pytest.mark.parametrize('entries', [ [(1, 12, 100.00), (1, 11, 100.00)] ]) def test_accounts(accounts, ledger, entries): for entry in entries: ledger.append((entry[DEBIT], entry[CREDIT], Decimal(entry[AMOUNT]))) assert len(ledger) == 2 assert ledger[0][DEBIT] == 1 assert ledger[0][CREDIT] == 12 assert ledger[0][AMOUNT] == Decimal(100.00) assert len(accounts) == 12 assert accounts[1].end == Decimal(200.00) assert accounts[11].end == Decimal(-100.00) assert accounts[12].end == Decimal(-100.00) print(ledger) print(accounts) 

Adicionado um novo teste test_accounts .


Execute o teste e observe a saída:


 General ledger 1 12 100.00 1 11 100.00 ---------------------- Account 1 beg: 0.00 0.00 12: 100.00 0.00 11: 100.00 0.00 end: 200.00 0.00 ---------------------- Account 11 beg: 0.00 0.00 1: 0.00 100.00 end: 0.00 100.00 ---------------------- Account 12 beg: 0.00 0.00 1: 0.00 100.00 end: 0.00 100.00 ---------------------- 

Nas classes Conta e Contas , os métodos __str__ também estão sobrecarregados, como você pode ver nas fontes do projeto. Os valores dos lançamentos e saldos para melhor clareza são apresentados em duas colunas: débito e crédito.


3. Contas: verificação de lançamento


Recordamos a seguinte regra:


         .         .   -         . 

Ou seja, em uma instância da classe Account , o valor final (saldo final) nas contas ativas não pode ser negativo e, nas contas passivas, não pode ser positivo.


Vá para a pasta day1 / step3 .


accounting.py :


 class BalanceException(Exception): pass 

Adicionada uma BalanceException .


 class Account: ... def is_active(self): return True if self.id < 5 else False def is_passive(self): return True if self.id > 8 else False ... 

Uma verificação foi adicionada à classe Conta para determinar se a conta está ativa ou passiva.


 class Accounts(dict): ... def check_balance(self, entry): if self[entry[CREDIT]].end - Decimal(entry[AMOUNT]) < 0 and self[entry[CREDIT]].is_active(): raise BalanceException('BalanceException') if self[entry[DEBIT]].end + Decimal(entry[AMOUNT]) > 0 and self[entry[DEBIT]].is_passive(): raise BalanceException('BalanceException') ... 

Uma verificação foi adicionada à classe Accounts.py , se, como resultado da adição de uma nova transação, um valor de débito negativo for gerado na conta ativa, uma exceção será gerada e a mesma coisa se uma conta passiva obtiver um valor de crédito negativo.


 class GeneralLedger(list): ... def append(self, entry): if self.accounts is not None: self.accounts.check_balance(entry) self.accounts.append_entry(entry) super().append(entry) ... 

Na classe GeneralLedger, antes de adicionar o lançamento às contas, realizamos uma verificação. Se uma exceção for gerada, o lançamento não se enquadra nas contas ou no Razão.


test_accounting.py :


 @pytest.mark.parametrize('entries, exception', [ ([(12, 1, 100.00)], BalanceException('BalanceException')), ([(12, 6, 100.00)], BalanceException('BalanceException')), ([(12, 11, 100.00)], BalanceException('BalanceException')), ([(6, 2, 100.00)], BalanceException('BalanceException')), #([(6, 7, 100.00)], BalanceException('BalanceException')), #([(6, 12, 100.00)], BalanceException('BalanceException')), ([(1, 2, 100.00)], BalanceException('BalanceException')), #([(1, 6, 100.00)], BalanceException('BalanceException')), #([(1, 12, 100.00)], BalanceException('BalanceException')), ]) def test_accounts_balance(accounts, ledger, entries, exception): for entry in entries: try: ledger.append((entry[DEBIT], entry[CREDIT], Decimal(entry[AMOUNT]))) except BalanceException as inst: assert isinstance(inst, type(exception)) assert inst.args == exception.args else: pytest.fail("Expected error but found none") assert len(ledger) == 0 assert len(accounts) == 12 

O teste test_accounts_balance foi adicionado ao módulo de teste. A lista de transações listou primeiro todas as combinações possíveis de transações e comentou todas as transações que não geram uma exceção. Execute o teste e verifique se as 5 opções de lançamento restantes geram uma BalanceException .


4. Equilíbrio


Vá para a pasta day1 / step4 .


accounting.py :


 class Balance(list): def __init__(self, accounts): self.accounts = accounts self.suma = Decimal(0.00) self.sump = Decimal(0.00) def create(self): self.suma = Decimal(0.00) self.sump = Decimal(0.00) for i in self.accounts.range: active = self.accounts[i].end if self.accounts[i].end >= 0 else Decimal(0.00) passive = -self.accounts[i].end if self.accounts[i].end < 0 else Decimal(0.00) self.append((active, passive)) self.suma += active self.sump += passive 

Ao criar um saldo, simplesmente coletamos os saldos de todas as contas em uma tabela.


test_accounting.py :


 @pytest.fixture def balance(accounts): return Balance(accounts) 

Crie uma trava de equilíbrio .


 @pytest.mark.parametrize('entries', [ [ ( 1, 12, 200.00), # increase active and passive ],[ ( 1, 12, 200.00), # increase active and passive (12, 1, 100.00), # decrease passive and decrease active ],[ ( 1, 12, 300.00), # increase active and passive (12, 1, 100.00), # decrease passive and decrease active ( 2, 1, 100.00), # increase active and decrease active ],[ ( 1, 12, 300.00), # increase active and passive (12, 1, 100.00), # decrease passive and decrease active ( 2, 1, 100.00), # increase active and decrease active (12, 11, 100.00), # decrease passive and increase passive ] ]) def test_balance(accounts, ledger, balance, entries): for entry in entries: ledger.append(entry) balance.create() print(accounts) print(balance) 

Nós criamos o teste test_balance . A lista de parâmetros listou todos os tipos possíveis de transações: aumentando o ativo e o passivo, diminuindo o ativo e o passivo, aumentando o ativo e diminuindo o ativo, aumentando o passivo e diminuindo o passivo. Foram emitidas 4 opções para lançamentos, para que você possa passo a passo ver a saída. Para a última opção, a saída é a seguinte:


 General ledger 1 12 300.00 12 1 100.00 2 1 100.00 12 11 100.00 ---------------------- Account 1 beg: 0.00 0.00 12: 300.00 0.00 12: 0.00 100.00 2: 0.00 100.00 end: 100.00 0.00 ---------------------- Account 2 beg: 0.00 0.00 1: 100.00 0.00 end: 100.00 0.00 ---------------------- Account 11 beg: 0.00 0.00 12: 0.00 100.00 end: 0.00 100.00 ---------------------- Account 12 beg: 0.00 0.00 1: 0.00 300.00 1: 100.00 0.00 11: 100.00 0.00 end: 0.00 100.00 ---------------------- Balance 1 : 100.00 0.00 2 : 100.00 0.00 3 : 0.00 0.00 4 : 0.00 0.00 5 : 0.00 0.00 6 : 0.00 0.00 7 : 0.00 0.00 8 : 0.00 0.00 9 : 0.00 0.00 10 : 0.00 0.00 11 : 0.00 100.00 12 : 0.00 100.00 ---------------------- sum: 200.00 200.00 ====================== 

5. Reverso


Agora vamos verificar como a reversão é realizada.


 @pytest.mark.parametrize('entries', [ [ ( 1, 12, 100.00), ( 1, 12,-100.00), ] ]) def test_storno(accounts, ledger, balance, entries): for entry in entries: ledger.append(entry) balance.create() print(ledger) print(accounts) print(balance) 

A conclusão foi a seguinte:


 General ledger 1 12 100.00 1 12 -100.00 ---------------------- Account 1 beg: 0.00 0.00 12: 100.00 0.00 12: 0.00 100.00 end: 0.00 0.00 ---------------------- Account 12 beg: 0.00 0.00 1: 0.00 100.00 1: 100.00 0.00 end: 0.00 0.00 ---------------------- Balance 1 : 0.00 0.00 2 : 0.00 0.00 3 : 0.00 0.00 4 : 0.00 0.00 5 : 0.00 0.00 6 : 0.00 0.00 7 : 0.00 0.00 8 : 0.00 0.00 9 : 0.00 0.00 10 : 0.00 0.00 11 : 0.00 0.00 12 : 0.00 0.00 ---------------------- sum: 0.00 0.00 ====================== 

Tudo parece estar certo.


E se usarmos esse conjunto de postagens, o teste será aprovado:


 ( 1, 12, 100.00), (12, 1, 100.00), ( 1, 12,-100.00), 

E se esse conjunto, trocar as duas últimas linhas em alguns lugares, obteremos uma exceção:


 ( 1, 12, 100.00), ( 1, 12,-100.00), (12, 1, 100.00), 

Portanto, para detectar esse erro, a reversão deve ser feita imediatamente após a correção da transação.


Conclusão


Nos artigos a seguir, continuaremos o estudo da contabilidade e consideraremos todos os aspectos do desenvolvimento do sistema de acordo com a lista de requisitos para o Empire ERP.

Source: https://habr.com/ru/post/pt471304/


All Articles