Introducción
Como desarrolladores, estamos familiarizados con estructuras de datos jerárquicas como árboles y arrays anidados. El Plan de Cuentas contable es esencialmente eso: una estructura de datos jerárquica que organiza todas las transacciones financieras de una empresa. Si alguna vez has implementado un sistema de comentarios anidados o categorías recursivas, ya tienes la base mental para entender este concepto.
En este tutorial, aprenderás a:
- Implementar un Plan de Cuentas usando modelos Django y MPTTModel
- Crear validaciones robustas para mantener la integridad contable
- Construir un sistema que permita gestionar todo desde el Admin de Django
Prerrequisitos
# Crear entorno virtual
python -m venv env
source env/bin/activate # Linux/Mac
env\Scripts\activate # Windows
# Instalar dependencias
pip install django==5.0
pip install django-mptt==0.15.0 # Para manejar datos jerárquicos
Conceptos Clave: De Arrays a Contabilidad
La Estructura Jerárquica
Piensa en el Plan de Cuentas como un árbol de directorios:
/activos
/activos_corrientes
/efectivo
/bancos
/activos_fijos
/edificios
/vehiculos
/pasivos
/pasivos_corrientes
/proveedores
/pasivos_largo_plazo
/prestamos
Los Tipos de Cuenta
Similar a cómo en programación tenemos tipos primitivos, en contabilidad tenemos tipos básicos de cuentas:
- Activo (A) → Como variables que almacenan valores positivos
- Pasivo (P) → Como variables que almacenan deudas
- Patrimonio (T) → Como el "resultado neto" del sistema
- Ingresos (I) → Como funciones que incrementan el patrimonio
- Gastos (G) → Como funciones que decrementan el patrimonio
Implementación en Django
Modelo Base
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from django.core.exceptions import ValidationError
from decimal import Decimal
class AccountType(models.TextChoices):
ASSET = 'A', 'Activo'
LIABILITY = 'P', 'Pasivo'
EQUITY = 'T', 'Patrimonio'
INCOME = 'I', 'Ingreso'
EXPENSE = 'G', 'Gasto'
class Account(MPTTModel):
code = models.CharField(
max_length=20,
unique=True,
help_text="Código único de la cuenta (ej: 1.1.1.01)"
)
name = models.CharField(
max_length=100,
help_text="Nombre descriptivo de la cuenta"
)
type = models.CharField(
max_length=1,
choices=AccountType.choices,
help_text="Tipo de cuenta"
)
parent = TreeForeignKey(
'self',
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='children'
)
is_transactional = models.BooleanField(
default=False,
help_text="Indica si la cuenta puede recibir transacciones directamente"
)
balance = models.DecimalField(
max_digits=15,
decimal_places=2,
default=Decimal('0.00'),
help_text="Saldo actual de la cuenta"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class MPTTMeta:
order_insertion_by = ['code']
class Meta:
verbose_name = 'Cuenta Contable'
verbose_name_plural = 'Cuentas Contables'
def __str__(self):
return f"{self.code} - {self.name}"
def clean(self):
if self.parent and self.type != self.parent.type:
raise ValidationError(
'El tipo de cuenta debe coincidir con el de su padre'
)
if self.is_transactional and self.children.exists():
raise ValidationError(
'Una cuenta transaccional no puede tener subcuentas'
)
if not self.is_transactional and not self.children.exists():
raise ValidationError(
'Una cuenta no transaccional debe tener subcuentas'
)
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
@property
def full_code(self):
"""Retorna el código completo incluyendo padres"""
ancestors = self.get_ancestors(include_self=True)
return '.'.join(a.code for a in ancestors)
def update_balance(self):
"""Actualiza el saldo de la cuenta y sus padres"""
if self.is_transactional:
# Para cuentas transaccionales, suma todas sus transacciones
self.balance = sum(
t.amount for t in self.transactions.all()
)
else:
# Para cuentas padre, suma los saldos de sus hijos
self.balance = sum(
child.balance for child in self.children.all()
)
self.save()
# Actualiza recursivamente los padres
if self.parent:
self.parent.update_balance()
Modelo para Transacciones
class Transaction(models.Model):
account = models.ForeignKey(
Account,
on_delete=models.PROTECT,
related_name='transactions'
)
date = models.DateField()
description = models.CharField(max_length=200)
amount = models.DecimalField(max_digits=15, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
def clean(self):
if not self.account.is_transactional:
raise ValidationError(
'Solo se pueden crear transacciones en cuentas transaccionales'
)
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
# Actualiza los saldos
self.account.update_balance()
class Meta:
verbose_name = 'Transacción'
verbose_name_plural = 'Transacciones'
Configuración del Admin
from django.contrib import admin
from mptt.admin import MPTTModelAdmin
from .models import Account, Transaction
@admin.register(Account)
class AccountAdmin(MPTTModelAdmin):
list_display = ('code', 'name', 'type', 'balance', 'is_transactional')
list_filter = ('type', 'is_transactional')
search_fields = ('code', 'name')
ordering = ('code',)
def get_readonly_fields(self, request, obj=None):
if obj: # editing an existing object
return ('balance',)
return ()
@admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin):
list_display = ('date', 'account', 'description', 'amount')
list_filter = ('date', 'account')
search_fields = ('description',)
date_hierarchy = 'date'
🧪 Tests Unitarios
from django.test import TestCase
from django.core.exceptions import ValidationError
from decimal import Decimal
from .models import Account, Transaction
class AccountTests(TestCase):
def setUp(self):
# Crear estructura básica de cuentas
self.assets = Account.objects.create(
code='1',
name='Activos',
type='A',
is_transactional=False
)
self.cash = Account.objects.create(
code='01',
name='Efectivo',
type='A',
parent=self.assets,
is_transactional=True
)
def test_balance_updates(self):
# Crear transacción
Transaction.objects.create(
account=self.cash,
date='2024-01-01',
description='Depósito inicial',
amount=Decimal('1000.00')
)
# Verificar balances
self.cash.refresh_from_db()
self.assets.refresh_from_db()
self.assertEqual(self.cash.balance, Decimal('1000.00'))
self.assertEqual(self.assets.balance, Decimal('1000.00'))
def test_invalid_transaction_account(self):
with self.assertRaises(ValidationError):
Transaction.objects.create(
account=self.assets, # cuenta no transaccional
date='2024-01-01',
description='Transacción inválida',
amount=Decimal('1000.00')
)
Ejemplo de Uso
Crear Plan de Cuentas Base
# En un management command o shell
def create_base_accounts():
# Activos
assets = Account.objects.create(
code='1', name='Activos', type='A', is_transactional=False
)
current_assets = Account.objects.create(
code='01', name='Activos Corrientes',
type='A', parent=assets, is_transactional=False
)
Account.objects.create(
code='001', name='Efectivo',
type='A', parent=current_assets, is_transactional=True
)
# Pasivos
liabilities = Account.objects.create(
code='2', name='Pasivos', type='P', is_transactional=False
)
# Etc...
Mejores Prácticas
-
Validaciones de Seguridad:
- Usar
PROTECT
enon_delete
para evitar eliminar cuentas con transacciones - Implementar validaciones a nivel de modelo
- Mantener los saldos actualizados automáticamente
- Usar
-
Manejo de Errores:
- Usar excepciones específicas para cada tipo de error
- Validar la integridad de los datos antes de guardar
- Mantener logs de todas las operaciones
-
Patrones de Diseño:
- Composite Pattern para la estructura jerárquica
- Observer Pattern para actualización de saldos
- Factory Pattern para crear cuentas predefinidas
Conclusión
Has aprendido a implementar un Plan de Cuentas robusto en Django, utilizando conceptos familiares de programación para entender principios contables. Este sistema permite:
- Mantener una estructura jerárquica de cuentas
- Validar automáticamente la integridad de los datos
- Gestionar todo desde el Admin de Django
- Mantener saldos actualizados automáticamente
Top comments (0)