DEV Community

Enrique Lazo Bello
Enrique Lazo Bello

Posted on

Implementando un Plan de Cuentas en Django: De Arrays a Asientos Contables 🎯

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

🧪 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')
            )
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

Mejores Prácticas

  1. Validaciones de Seguridad:

    • Usar PROTECT en on_delete para evitar eliminar cuentas con transacciones
    • Implementar validaciones a nivel de modelo
    • Mantener los saldos actualizados automáticamente
  2. 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
  3. 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)