En este artículo, intentaremos penetrar en el corazón mismo de la "empresa sangrienta": la contabilidad. Primero, realizaremos un estudio del libro mayor, cuentas y balance general, identificaremos sus propiedades y algoritmos inherentes. Utilizamos la tecnología Python y Test Driven Development. Aquí nos dedicaremos a la creación de prototipos, por lo que en lugar de la base de datos usaremos los contenedores básicos: listas, diccionarios y tuplas. El proyecto se está desarrollando de acuerdo con los requisitos del proyecto Empire ERP .
Condición de la tarea
Espacio ... Planeta de Empireia ... Un estado en todo el planeta. La población trabaja 2 horas en 2 semanas, después de 2 años para jubilarse. El plan de cuentas consta de 12 posiciones. Las cuentas 1-4 están activas, 5-8 son activas-pasivas, 9-12 son pasivas. Empresa de cuernos y pezuñas. Todas las transacciones se llevan a cabo en un período de informe, al comienzo del período no hay saldos.
Configuración del proyecto
Clonamos el proyecto desde el github:
git clone https://github.com/nomhoi/empire-erp.git
Estamos desarrollando en Python 3.7.4. Configure el entorno virtual, actívelo e instale pytest .
pip install pytest
1. Libro mayor
Vaya a la carpeta reaserch / day1 / step1 .
contabilidad.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)
El libro principal, como vemos, se presenta en forma de una lista de registros. Cada entrada está diseñada como una tupla. Para el registro de la transacción hasta el momento, utilizamos solo los números de cuenta para débito y crédito y el monto de la transacción. Las fechas, descripciones y otra información aún no son necesarias, las agregaremos más adelante.
El pestillo del libro mayor y la prueba parametrizada test_ledger se crearon en el archivo de prueba. En el parámetro de prueba de entradas , transferimos inmediatamente la lista completa de transacciones. Para verificar, ejecutamos el comando pytest -s -v en la terminal. La prueba debe pasar, y veremos en el terminal la lista completa de transacciones almacenadas en el libro mayor:
General ledger 1 12 100.00 1 11 100.00
2. Cuentas
Ahora agreguemos soporte de facturas al proyecto. Vaya a la carpeta día1 / paso2 .
contabilidad.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)
La clase GeneralLedger sobrecargó el método append . Cuando agrega una transacción a un libro, la agregamos inmediatamente a las cuentas.
contabilidad.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]))
La clase Cuentas está diseñada como un diccionario. En las claves, el número de cuenta, en los valores, el contenido de la cuenta, es decir una instancia de la clase Cuenta , que a su vez contiene los campos de saldo inicial y final y una lista de transacciones relacionadas con esta cuenta. Tenga en cuenta que en esta lista los montos de las entradas de débito y crédito se almacenan en un campo, el monto de débito es positivo, el monto del préstamo es negativo.
test_accounting.py :
@pytest.fixture def accounts(): return Accounts() @pytest.fixture def ledger(accounts): return GeneralLedger(accounts)
En el archivo de prueba, agregamos el bloqueo de cuentas y ajustamos el bloqueo del libro mayor .
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)
Se agregó una nueva prueba test_accounts .
Ejecute la prueba y observe la salida:
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 ----------------------
En las clases Cuenta y Acconts , los métodos __str__ también están sobrecargados, puede ver en las fuentes del proyecto. Las cantidades de contabilizaciones y saldos para mayor claridad se presentan en dos columnas: débito y crédito.
3. Cuentas: verificación de contabilización
Recordamos la siguiente regla:
. . - .
Es decir, en una instancia de la clase Cuenta , el valor final (saldo final) en cuentas activas no puede ser negativo, y en cuentas pasivas no puede ser positivo.
Vaya a la carpeta día1 / paso3 .
contabilidad.py :
class BalanceException(Exception): pass
Se agregó una 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 ...
Se agregó un cheque a la clase Cuenta para determinar si la cuenta está activa o pasiva.
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') ...
Se agregó un cheque a la clase Accounts.py , si como resultado de agregar una nueva transacción se genera un valor de débito negativo en la cuenta activa, se generará una excepción, y lo mismo si la cuenta pasiva recibe un 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) ...
En la clase GeneralLedger, antes de agregar la publicación a las cuentas, realizamos una verificación. Si se produce una excepción, la contabilización no cae ni en las cuentas ni en el libro mayor.
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
La prueba test_accounts_balance se agregó al módulo de prueba. La lista de transacciones enumeró primero todas las combinaciones posibles de transacciones y comentó todas las transacciones que no generan una excepción. Ejecute la prueba y asegúrese de que las 5 opciones de publicación restantes generen una excepción BalanceException .
4. Balance
Vaya a la carpeta día1 / paso4 .
contabilidad.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
Al crear un saldo, simplemente recopilamos los saldos de todas las cuentas en una tabla.
test_accounting.py :
@pytest.fixture def balance(accounts): return Balance(accounts)
Crea un pestillo de equilibrio .
@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)
Creamos la prueba test_balance . En las listas de parámetros, se enumeraron todos los tipos posibles de transacciones: aumentar el activo y el pasivo, disminuir el activo y el pasivo, aumentar el activo y disminuir el activo, aumentar el pasivo y disminuir el pasivo. Emitió 4 opciones para publicaciones, para que pueda ver paso a paso el resultado. Para la última opción, el resultado es el siguiente:
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. Invertir
Ahora veamos cómo se realiza la inversión.
@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)
La conclusión fue la siguiente:
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 ======================
Todo parece estar bien.
Y si usamos este conjunto de publicaciones, la prueba pasará:
( 1, 12, 100.00), (12, 1, 100.00), ( 1, 12,-100.00),
Y si tal conjunto, intercambia las últimas 2 líneas en lugares, entonces obtenemos una excepción:
( 1, 12, 100.00), ( 1, 12,-100.00), (12, 1, 100.00),
Por lo tanto, para detectar dicho error, la reversión debe realizarse inmediatamente después de que se corrija la transacción.
Conclusión
En los siguientes artículos continuaremos el estudio de la contabilidad y consideraremos todos los aspectos del desarrollo del sistema de acuerdo con la lista de requisitos para Empire ERP.