Contabilidad para Django Developers: Implementando un Plan de Cuentas Jerárquico
Introducción
Si has trabajado con bases de datos jerárquicas o estructuras de árbol en programación, entonces ya tienes la base mental para entender un Plan de Cuentas contable. Imagina un sistema de archivos: tienes directorios principales (Grupos), subdirectorios (Clases), y archivos (Cuentas), cada uno con sus propias características y restricciones.
En este tutorial, traduciremos conceptos contables a términos de programación, implementando un Plan de Cuentas robusto que maneje la jerarquía natural de la contabilidad: Grupo → Clase → Cuenta → Subcuenta. Aprenderás a crear un sistema que mantenga la integridad de los datos contables mientras preserva las relaciones jerárquicas entre cuentas.
Prerrequisitos
# Crear entorno virtual
python -m venv env
source env/bin/activate
# Instalar dependencias
pip install django==5.0
Conceptos Clave
La Jerarquía Contable como Estructura de Árbol
# Analogía con sistema de archivos
root/
├── activos/ # Grupo
│ ├── corrientes/ # Clase
│ │ ├── efectivo/ # Cuenta
│ │ │ ├── caja # Subcuenta
│ │ │ └── bancos # Subcuenta
│ │ └── inventario
│ └── fijos/
├── pasivos/
└── patrimonio/
Implementación en Django
from decimal import Decimal, ROUND_HALF_UP
from django.db import models
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
class AccountLevel(models.TextChoices):
"""Niveles jerárquicos del plan de cuentas"""
GROUP = 'G', 'Grupo'
CLASS = 'C', 'Clase'
ACCOUNT = 'A', 'Cuenta'
SUBACCOUNT = 'S', 'Subcuenta'
class AccountType(models.TextChoices):
"""Tipos principales de cuentas"""
ASSET = 'A', 'Activo'
LIABILITY = 'P', 'Pasivo'
EQUITY = 'E', 'Patrimonio'
INCOME = 'I', 'Ingreso'
EXPENSE = 'G', 'Gasto'
class AccountNature(models.TextChoices):
"""Naturaleza de la cuenta (determina su comportamiento)"""
DEBIT = 'D', 'Deudora'
CREDIT = 'C', 'Acreedora'
class Account(models.Model):
"""
Modelo principal para el Plan de Cuentas.
Implementa una estructura jerárquica recursiva.
"""
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"
)
level = models.CharField(
max_length=1,
choices=AccountLevel.choices,
help_text="Nivel jerárquico de la cuenta"
)
type = models.CharField(
max_length=1,
choices=AccountType.choices,
help_text="Tipo de cuenta"
)
nature = models.CharField(
max_length=1,
choices=AccountNature.choices,
help_text="Naturaleza de la cuenta (Deudora/Acreedora)"
)
parent = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.PROTECT,
related_name='children'
)
is_transactional = models.BooleanField(
default=False,
help_text="Indica si la cuenta puede recibir transacciones"
)
balance = models.DecimalField(
max_digits=15,
decimal_places=2,
default=Decimal('0.00'),
validators=[
MinValueValidator(Decimal('-999999999999.99')),
MaxValueValidator(Decimal('999999999999.99'))
],
help_text="Saldo actual de la cuenta"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
class Meta:
verbose_name = 'Cuenta Contable'
verbose_name_plural = 'Cuentas Contables'
ordering = ['code']
def __str__(self):
return f"{self.code} - {self.name}"
def clean(self):
"""Validaciones de negocio para la cuenta"""
self._validate_hierarchy()
self._validate_code_format()
self._validate_account_type()
self._validate_transaction_rules()
def _validate_hierarchy(self):
"""Valida la jerarquía de la cuenta"""
if self.parent:
# Validar nivel jerárquico
level_order = {
'G': 0, 'C': 1, 'A': 2, 'S': 3
}
if level_order[self.level] <= level_order[self.parent.level]:
raise ValidationError(
'El nivel jerárquico debe ser mayor al del padre'
)
# Validar tipo de cuenta
if self.type != self.parent.type:
raise ValidationError(
'El tipo de cuenta debe coincidir con el de su padre'
)
else:
# Solo grupos pueden no tener padre
if self.level != 'G':
raise ValidationError(
'Solo los grupos pueden no tener cuenta padre'
)
def _validate_code_format(self):
"""Valida el formato del código según el nivel"""
code_patterns = {
'G': r'^\d$', # Un dígito: 1
'C': r'^\d\.\d{2}$', # Tres dígitos: 1.01
'A': r'^\d\.\d{2}\.\d{2}$', # Cinco dígitos: 1.01.01
'S': r'^\d\.\d{2}\.\d{2}\.\d{2}$' # Siete dígitos: 1.01.01.01
}
if not re.match(code_patterns[self.level], self.code):
raise ValidationError(
f'Formato de código inválido para nivel {self.get_level_display()}'
)
def _validate_account_type(self):
"""Valida la naturaleza según el tipo de cuenta"""
nature_by_type = {
'A': 'D', # Activos son deudores
'P': 'C', # Pasivos son acreedores
'E': 'C', # Patrimonio es acreedor
'I': 'C', # Ingresos son acreedores
'G': 'D' # Gastos son deudores
}
if self.nature != nature_by_type[self.type]:
raise ValidationError(
f'La naturaleza no corresponde al tipo de cuenta'
)
def _validate_transaction_rules(self):
"""Valida reglas para cuentas transaccionales"""
if self.is_transactional:
if self.level != 'S':
raise ValidationError(
'Solo las subcuentas pueden ser transaccionales'
)
if self.children.exists():
raise ValidationError(
'Una cuenta transaccional no puede tener subcuentas'
)
elif self.level == 'S':
raise ValidationError(
'Las subcuentas deben ser transaccionales'
)
def update_balance(self):
"""Actualiza el saldo de la cuenta y propaga hacia arriba"""
if self.is_transactional:
# Calcular saldo desde las transacciones
transactions = self.transactions.all()
debits = sum(t.amount for t in transactions if t.entry_type == 'D')
credits = sum(t.amount for t in transactions if t.entry_type == 'C')
# Aplicar según naturaleza de la cuenta
if self.nature == 'D':
self.balance = debits - credits
else:
self.balance = credits - debits
else:
# Para cuentas no transaccionales, sumar saldos de hijos
self.balance = sum(child.balance for child in self.children.all())
self.save()
# Propagar hacia arriba
if self.parent:
self.parent.update_balance()
@property
def absolute_balance(self):
"""Retorna el saldo considerando la naturaleza de la cuenta"""
if self.nature == 'D':
return self.balance
return -self.balance
def get_descendants(self):
"""Retorna todas las cuentas descendientes"""
descendants = []
for child in self.children.all():
descendants.append(child)
descendants.extend(child.get_descendants())
return descendants
def get_ancestors(self):
"""Retorna todas las cuentas ancestras"""
ancestors = []
parent = self.parent
while parent:
ancestors.append(parent)
parent = parent.parent
return ancestors
Configuración del Admin
from django.contrib import admin
from .models import Account
@admin.register(Account)
class AccountAdmin(admin.ModelAdmin):
list_display = [
'code', 'name', 'level', 'type', 'nature',
'is_transactional', 'balance', 'active'
]
list_filter = ['level', 'type', 'nature', 'is_transactional', 'active']
search_fields = ['code', 'name']
readonly_fields = ['balance', 'created_at', 'updated_at']
def get_readonly_fields(self, request, obj=None):
if obj: # editing an existing object
return self.readonly_fields + ['code', 'level', 'type', 'nature']
return self.readonly_fields
def get_queryset(self, request):
return super().get_queryset(request).select_related('parent')
Tests Unitarios
from django.test import TestCase
from django.core.exceptions import ValidationError
from decimal import Decimal
from .models import Account
class AccountTests(TestCase):
def setUp(self):
# Crear estructura básica
self.assets = Account.objects.create(
code='1',
name='Activos',
level='G',
type='A',
nature='D'
)
self.current_assets = Account.objects.create(
code='1.01',
name='Activos Corrientes',
level='C',
type='A',
nature='D',
parent=self.assets
)
def test_hierarchy_validation(self):
"""Prueba las validaciones jerárquicas"""
with self.assertRaises(ValidationError):
# Intentar crear una clase sin padre
Account.objects.create(
code='2.01',
name='Invalid',
level='C',
type='A',
nature='D'
)
def test_code_format(self):
"""Prueba la validación de formato de código"""
with self.assertRaises(ValidationError):
Account.objects.create(
code='1.1', # Formato inválido para clase
name='Invalid',
level='C',
type='A',
nature='D',
parent=self.assets
)
def test_balance_propagation(self):
"""Prueba la propagación de saldos"""
# Crear cuenta transaccional
cash = Account.objects.create(
code='1.01.01.01',
name='Caja General',
level='S',
type='A',
nature='D',
parent=self.current_assets,
is_transactional=True
)
# Simular transacción
cash.balance = Decimal('1000.00')
cash.save()
cash.update_balance()
# Verificar propagación
self.current_assets.refresh_from_db()
self.assets.refresh_from_db()
self.assertEqual(self.current_assets.balance, Decimal('1000.00'))
self.assertEqual(self.assets.balance, Decimal('1000.00'))
Ejemplo Real: Creación del Plan de Cuentas Base
def create_base_chart_of_accounts():
"""Crea una estructura básica del plan de cuentas"""
# Activos
assets = Account.objects.create(
code='1',
name='Activos',
level='G',
type='A',
nature='D'
)
current_assets = Account.objects.create(
code='1.01',
name='Activos Corrientes',
level='C',
type='A',
nature='D',
parent=assets
)
cash = Account.objects.create(
code='1.01.01',
name='Efectivo y Equivalentes',
level='A',
type='A',
nature='D',
parent=current_assets
)
Account.objects.create(
code='1.01.01.01',
name='Caja General',
level='S',
type='A',
nature='D',
parent=cash,
is_transactional=True
)
# Pasivos
liabilities = Account.objects.create(
code='2',
name='Pasivos',
level='G',
type='P',
nature='C'
)
# ... continuar con la estructura
Mejores Prácticas
-
Validaciones de Seguridad
- Proteger la integridad jerárquica
- Validar formatos de código
- Controlar la naturaleza de las cuentas
- Prevenir modificaciones no autorizadas
-
Manejo de Errores
- Validaciones específicas por nivel
- Mensajes de error claros
- Control de saldos y balances
-
Patrones de Diseño
- Composite para estructura jerárquica
- Observer para actualización de saldos
- Template Method para validaciones
Conclusión
Has aprendido a implementar un Plan de Cuentas que:
- Mantiene una estructura jerárquica sólida
- Valida la integridad de los datos contables
- Propaga correctamente los saldos
- Permite una gestión eficiente desde el Admin de Django
- Implementa validaciones robustas de negocio
- Mantiene la consistencia de los datos contables
Top comments (0)