Introducción
Como desarrollador Django, ¿alguna vez te has enfrentado al desafío de implementar un sistema contable y te has sentido abrumado por la terminología financiera? No estás solo. La contabilidad puede parecer un mundo completamente diferente a la programación, pero en realidad, comparte muchos conceptos similares a los que ya conoces.
En este tutorial, aprenderás a implementar un sistema de estados financieros utilizando Django y su potente admin interface, traduciendo conceptos contables a términos de programación que ya conoces. Por ejemplo, verás cómo un asiento contable es similar a una transacción en una base de datos, y cómo el principio de partida doble es comparable a mantener la integridad referencial.
Al final de este tutorial, serás capaz de implementar un sistema contable básico pero robusto, con validaciones automáticas y tests unitarios, todo desde el Django Admin.
Prerrequisitos
- Python 3.12
- Django 5.0
- Conocimientos básicos de modelos Django y Django Admin
- SQLite o PostgreSQL para la base de datos
Conceptos Clave
La Contabilidad desde una Perspectiva de Programador
Antes de sumergirnos en el código, vamos a establecer algunas analogías:
-
Balance General = Estado de la Aplicación
- Similar a cómo guardamos el estado de una aplicación
- Activo = Recursos (como RAM o espacio en disco)
- Pasivo = Recursos comprometidos (como procesos en background)
- Patrimonio = Recursos netos disponibles
-
Estado de Resultados = Logs de Operaciones
- Como un log de transacciones
- Ingresos = Datos entrantes
- Gastos = Recursos consumidos
- Utilidad = Resultado final del proceso
Implementación
Modelos Base
from django.db import models
from django.core.exceptions import ValidationError
from django.db.models import Sum
from decimal import Decimal
import uuid
class AccountType(models.TextChoices):
ASSET = 'ASSET', 'Activo'
LIABILITY = 'LIABILITY', 'Pasivo'
EQUITY = 'EQUITY', 'Patrimonio'
INCOME = 'INCOME', 'Ingreso'
EXPENSE = 'EXPENSE', 'Gasto'
class Account(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
code = models.CharField(max_length=20, unique=True)
name = models.CharField(max_length=100)
type = models.CharField(max_length=10, choices=AccountType.choices)
balance = models.DecimalField(max_digits=15, decimal_places=2, default=0)
parent = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.PROTECT,
related_name='children'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['code']
indexes = [
models.Index(fields=['code']),
models.Index(fields=['type']),
]
def __str__(self):
return f"{self.code} - {self.name}"
def clean(self):
if self.parent and self.parent.type != self.type:
raise ValidationError('La cuenta padre debe ser del mismo tipo.')
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
@property
def calculated_balance(self):
"""Calcula el balance incluyendo subcuentas"""
balance = self.balance
for child in self.children.all():
balance += child.calculated_balance
return balance
class JournalEntry(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
date = models.DateField()
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Asiento Contable'
verbose_name_plural = 'Asientos Contables'
ordering = ['-date', '-created_at']
def __str__(self):
return f"Asiento #{self.id} - {self.date}"
def clean(self):
if hasattr(self, 'entries'):
debits = sum(entry.debit for entry in self.entries.all())
credits = sum(entry.credit for entry in self.entries.all())
if debits != credits:
raise ValidationError(
'Los débitos y créditos deben ser iguales.'
)
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
class JournalEntryLine(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
entry = models.ForeignKey(
JournalEntry,
on_delete=models.CASCADE,
related_name='entries'
)
account = models.ForeignKey(
Account,
on_delete=models.PROTECT,
related_name='entries'
)
debit = models.DecimalField(max_digits=15, decimal_places=2, default=0)
credit = models.DecimalField(max_digits=15, decimal_places=2, default=0)
description = models.CharField(max_length=200, blank=True)
class Meta:
verbose_name = 'Línea de Asiento'
verbose_name_plural = 'Líneas de Asiento'
def clean(self):
if self.debit < 0 or self.credit < 0:
raise ValidationError(
'Los valores no pueden ser negativos.'
)
if self.debit > 0 and self.credit > 0:
raise ValidationError(
'Una línea no puede tener débito y crédito simultáneamente.'
)
if self.debit == 0 and self.credit == 0:
raise ValidationError(
'Debe especificar un débito o un crédito.'
)
def save(self, *args, **kwargs):
self.full_clean()
# Actualizar balance de la cuenta
if self._state.adding: # Solo para nuevos registros
if self.debit > 0:
self.account.balance += self.debit
else:
self.account.balance -= self.credit
self.account.save()
super().save(*args, **kwargs)
Configuración del Admin
from django.contrib import admin
from django.db import models
from .models import Account, JournalEntry, JournalEntryLine
class AccountAdmin(admin.ModelAdmin):
list_display = ['code', 'name', 'type', 'balance', 'parent']
list_filter = ['type']
search_fields = ['code', 'name']
readonly_fields = ['balance']
class JournalEntryLineInline(admin.TabularInline):
model = JournalEntryLine
extra = 2
fields = ['account', 'debit', 'credit', 'description']
class JournalEntryAdmin(admin.ModelAdmin):
list_display = ['id', 'date', 'description', 'total_debit', 'total_credit']
inlines = [JournalEntryLineInline]
def total_debit(self, obj):
return sum(line.debit for line in obj.entries.all())
def total_credit(self, obj):
return sum(line.credit for line in obj.entries.all())
admin.site.register(Account, AccountAdmin)
admin.site.register(JournalEntry, JournalEntryAdmin)
Tests Unitarios
from django.test import TestCase
from django.core.exceptions import ValidationError
from decimal import Decimal
from .models import Account, JournalEntry, JournalEntryLine
class AccountingTests(TestCase):
def setUp(self):
# Crear cuentas de prueba
self.cash = Account.objects.create(
code='1000',
name='Caja',
type='ASSET'
)
self.bank = Account.objects.create(
code='1001',
name='Banco',
type='ASSET'
)
self.capital = Account.objects.create(
code='3000',
name='Capital',
type='EQUITY'
)
def test_double_entry_principle(self):
"""Prueba el principio de partida doble"""
entry = JournalEntry.objects.create(
date='2024-01-01',
description='Aporte inicial'
)
# Crear líneas de asiento
JournalEntryLine.objects.create(
entry=entry,
account=self.cash,
debit=Decimal('1000.00')
)
JournalEntryLine.objects.create(
entry=entry,
account=self.capital,
credit=Decimal('1000.00')
)
# Verificar balances
self.cash.refresh_from_db()
self.capital.refresh_from_db()
self.assertEqual(self.cash.balance, Decimal('1000.00'))
self.assertEqual(self.capital.balance, Decimal('-1000.00'))
def test_invalid_entry(self):
"""Prueba validaciones de asientos inválidos"""
entry = JournalEntry.objects.create(
date='2024-01-01',
description='Asiento inválido'
)
with self.assertRaises(ValidationError):
JournalEntryLine.objects.create(
entry=entry,
account=self.cash,
debit=Decimal('-1000.00') # Valor negativo
)
Ejemplo Práctico: Sistema de Transferencias
1. Crear las Cuentas Base
Desde el admin de Django, crea las siguientes cuentas:
- Activos
- Caja (1000)
- Banco (1001)
- Pasivos
- Cuentas por Pagar (2000)
- Patrimonio
- Capital (3000)
- Ingresos
- Ventas (4000)
- Gastos
- Gastos Operativos (5000)
2. Registrar una Transacción
Para registrar una venta en efectivo:
- Crear nuevo JournalEntry
- Fecha: La fecha de la transacción
- Descripción: "Venta al contado"
- Agregar líneas:
- Débito a Caja (1000) por $100
- Crédito a Ventas (4000) por $100
3. Verificar Balances
# Desde el shell de Django
from accounting.models import Account
# Verificar balance de caja
caja = Account.objects.get(code='1000')
print(f"Balance de caja: ${caja.balance}")
# Verificar balance de ventas
ventas = Account.objects.get(code='4000')
print(f"Balance de ventas: ${ventas.balance}")
Mejores Prácticas
-
Validaciones de Seguridad
- Usar transacciones de base de datos
- Implementar permisos granulares en el admin
- Mantener log de cambios
-
Manejo de Errores
- Validar balances negativos según tipo de cuenta
- Verificar referencias circulares en cuentas
- Implementar rollback en caso de error
-
Patrones de Diseño
- Usar Factory Method para crear asientos predefinidos
- Implementar Observer para actualizar balances
- Aplicar Command para operaciones complejas
Conclusión
Has aprendido a implementar un sistema contable básico usando Django y su admin interface. Los conceptos clave que debes recordar son:
- La contabilidad es similar a mantener estado en tu aplicación
- El principio de partida doble garantiza la integridad de los datos
- Las validaciones automáticas previenen errores comunes
Top comments (0)