🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 12 Min Lesezeit
0

Contabilidad para Django Developers: Control de Cambios y Tipo de Cambio

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht




Introducción



Como desarrollador Django, ¿alguna vez te has preguntado cómo manejar correctamente las operaciones en diferentes monedas en tus aplicaciones? Si estás construyendo sistemas empresariales o financieros, entender el control de cambios no es solo una necesidad contable - es un requisito técnico crucial.



En este tutorial, transformaremos conceptos contables complejos en modelos Django intuitivos, usando el admin de Django como nuestra interfaz principal. Aprenderás a manejar tipos de cambio, calcular diferencias cambiarias y mantener una posición de cambio precisa, todo sin escribir una sola vista personalizada.



Al finalizar, tendrás un sistema robusto de control de cambios que cualquier contador podrá usar desde el admin de Django, con validaciones automáticas y tests que aseguran su correcto funcionamiento.






Prerrequisitos




  • Python 3.12+

  • Django 5.0+

  • Conocimientos básicos de modelos Django y el admin

  • SQLite o PostgreSQL

  • Git para control de versiones






Conceptos Clave






El Tipo de Cambio como un State Pattern



Piensa en el tipo de cambio como un state pattern en programación: representa el estado de una moneda en relación a otra en un momento específico. Así como guardamos timestamps para auditoría, necesitamos mantener un historial de tipos de cambio para operaciones financieras.




CODE
# Analogía en código
class Currency(str, Enum):
PEN = "PEN" # Soles
USD = "USD" # Dólares
EUR = "EUR" # Euros

# El tipo de cambio es como un estado inmutable
@dataclass(frozen=True)
class ExchangeRate:
currency_from: Currency
currency_to: Currency
rate: Decimal
timestamp: datetime









Diferencia de Cambio como Debugging



La diferencia de cambio es similar a encontrar un bug entre el comportamiento esperado y el actual. Cuando tienes una cuenta por pagar de $1000 registrada cuando el dólar estaba a S/3.80, pero la pagas cuando está a S/3.85, esa diferencia necesita ser registrada - como cuando haces un diff entre versiones de código.






Implementación Práctica






Modelos Base






CODE
from django.db import models
from django.core.validators import MinValueValidator
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.utils import timezone

class Currency(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=50)
is_base = models.BooleanField(default=False)

def save(self, *args, **kwargs):
if self.is_base:
# Solo puede haber una moneda base
Currency.objects.filter(is_base=True).update(is_base=False)
super().save(*args, **kwargs)

def __str__(self):
return f"{self.code} - {self.name}"

class Meta:
verbose_name_plural = "Currencies"

class ExchangeRate(models.Model):
currency_from = models.ForeignKey(
Currency,
on_delete=models.PROTECT,
related_name='rates_from'
)
currency_to = models.ForeignKey(
Currency,
on_delete=models.PROTECT,
related_name='rates_to'
)
rate = models.DecimalField(
max_digits=10,
decimal_places=4,
validators=[MinValueValidator(Decimal('0.0001'))]
)
date = models.DateField(default=timezone.now)
is_sbs = models.BooleanField(
default=False,
verbose_name="Is SBS Rate",
help_text="Indica si es tipo de cambio oficial SBS"
)

class Meta:
unique_together = [['currency_from', 'currency_to', 'date']]

def clean(self):
if self.currency_from == self.currency_to:
raise ValidationError(
"La moneda origen y destino no pueden ser iguales"
)
if self.is_sbs and ExchangeRate.objects.filter(
date=self.date,
is_sbs=True,
currency_from=self.currency_from,
currency_to=self.currency_to
).exclude(id=self.id).exists():
raise ValidationError(
"Ya existe un tipo de cambio SBS para esta fecha"
)

def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)

# Crear tipo de cambio inverso automáticamente
if not ExchangeRate.objects.filter(
currency_from=self.currency_to,
currency_to=self.currency_from,
date=self.date
).exists():
ExchangeRate.objects.create(
currency_from=self.currency_to,
currency_to=self.currency_from,
rate=Decimal('1.0') / self.rate,
date=self.date,
is_sbs=self.is_sbs
)

class ForexAccount(models.Model):
currency = models.ForeignKey(Currency, on_delete=models.PROTECT)
code = models.CharField(max_length=10, unique=True)
name = models.CharField(max_length=100)
balance = models.DecimalField(
max_digits=15,
decimal_places=2,
default=Decimal('0.00')
)

def __str__(self):
return f"{self.code} - {self.name} ({self.currency.code})"

def get_balance_base_currency(self, date=None):
if not date:
date = timezone.now().date()

base_currency = Currency.objects.get(is_base=True)
if self.currency == base_currency:
return self.balance

try:
rate = ExchangeRate.objects.get(
currency_from=self.currency,
currency_to=base_currency,
date=date
)
return self.balance * rate.rate
except ExchangeRate.DoesNotExist:
raise ValidationError(
f"No existe tipo de cambio para {date}"
)

class ForexTransaction(models.Model):
TRANSACTION_TYPES = [
('IN', 'Ingreso'),
('OUT', 'Egreso'),
]

account = models.ForeignKey(
ForexAccount,
on_delete=models.PROTECT,
related_name='transactions'
)
type = models.CharField(max_length=3, choices=TRANSACTION_TYPES)
amount = models.DecimalField(max_digits=15, decimal_places=2)
exchange_rate = models.ForeignKey(
ExchangeRate,
on_delete=models.PROTECT,
null=True,
blank=True
)
date = models.DateField(default=timezone.now)
description = models.TextField()

def clean(self):
if self.exchange_rate and (
self.exchange_rate.currency_from != self.account.currency
):
raise ValidationError(
"El tipo de cambio debe corresponder a la moneda de la cuenta"
)

def save(self, *args, **kwargs):
self.full_clean()
if not self.exchange_rate:
base_currency = Currency.objects.get(is_base=True)
self.exchange_rate = ExchangeRate.objects.get(
currency_from=self.account.currency,
currency_to=base_currency,
date=self.date
)

# Actualizar saldo de la cuenta
multiplier = Decimal('1.0') if self.type == 'IN' else Decimal('-1.0')
self.account.balance += (self.amount * multiplier)
self.account.save()

super().save(*args, **kwargs)









Configuración del Admin






CODE
from django.contrib import admin
from django.utils.html import format_html

@admin.register(Currency)
class CurrencyAdmin(admin.ModelAdmin):
list_display = ['code', 'name', 'is_base']
search_fields = ['code', 'name']
list_filter = ['is_base']

@admin.register(ExchangeRate)
class ExchangeRateAdmin(admin.ModelAdmin):
list_display = [
'date',
'currency_from',
'currency_to',
'rate',
'is_sbs'
]
list_filter = ['date', 'currency_from', 'currency_to', 'is_sbs']
search_fields = ['currency_from__code', 'currency_to__code']
date_hierarchy = 'date'

@admin.register(ForexAccount)
class ForexAccountAdmin(admin.ModelAdmin):
list_display = [
'code',
'name',
'currency',
'balance',
'base_currency_balance'
]
search_fields = ['code', 'name']
list_filter = ['currency']

def base_currency_balance(self, obj):
try:
base_balance = obj.get_balance_base_currency()
base_currency = Currency.objects.get(is_base=True)
return f"{base_currency.code} {base_balance:.2f}"
except Exception as e:
return format_html(
'<span style="color: red;">{}</span>',
str(e)
)

base_currency_balance.short_description = "Balance en Moneda Base"

@admin.register(ForexTransaction)
class ForexTransactionAdmin(admin.ModelAdmin):
list_display = [
'date',
'account',
'type',
'amount',
'exchange_rate',
'amount_base_currency'
]
list_filter = ['date', 'type', 'account__currency']
search_fields = ['description', 'account__name']
date_hierarchy = 'date'

def amount_base_currency(self, obj):
base_amount = obj.amount * obj.exchange_rate.rate
base_currency = Currency.objects.get(is_base=True)
return f"{base_currency.code} {base_amount:.2f}"

amount_base_currency.short_description = "Monto en Moneda Base"









Tests Unitarios






CODE
from django.test import TestCase
from django.core.exceptions import ValidationError
from decimal import Decimal
from django.utils import timezone
from .models import Currency, ExchangeRate, ForexAccount, ForexTransaction

class ForexTests(TestCase):
def setUp(self):
# Crear monedas base
self.pen = Currency.objects.create(
code='PEN',
name='Soles',
is_base=True
)
self.usd = Currency.objects.create(
code='USD',
name='Dólares'
)

# Crear tipo de cambio
self.exchange_rate = ExchangeRate.objects.create(
currency_from=self.usd,
currency_to=self.pen,
rate=Decimal('3.85'),
date=timezone.now().date(),
is_sbs=True
)

# Crear cuenta
self.account = ForexAccount.objects.create(
currency=self.usd,
code='1041',
name='Cuenta Corriente USD'
)

def test_exchange_rate_inverse(self):
"""Verifica que se cree automáticamente el tipo de cambio inverso"""
inverse_rate = ExchangeRate.objects.get(
currency_from=self.pen,
currency_to=self.usd,
date=self.exchange_rate.date
)
self.assertAlmostEqual(
float(inverse_rate.rate),
float(Decimal('1.0') / self.exchange_rate.rate),
places=4
)

def test_account_balance_update(self):
"""Verifica la actualización correcta del saldo"""
initial_balance = self.account.balance

# Crear transacción de ingreso
transaction = ForexTransaction.objects.create(
account=self.account,
type='IN',
amount=Decimal('1000.00'),
exchange_rate=self.exchange_rate,
description='Test transaction'
)

self.account.refresh_from_db()
self.assertEqual(
self.account.balance,
initial_balance + Decimal('1000.00')
)

# Verificar balance en moneda base
base_balance = self.account.get_balance_base_currency()
self.assertEqual(
base_balance,
Decimal('1000.00') * Decimal('3.85')
)

def test_invalid_exchange_rate(self):
"""Verifica que no se pueden crear tipos de cambio inválidos"""
with self.assertRaises(ValidationError):
ExchangeRate.objects.create(
currency_from=self.usd,
currency_to=self.usd,
rate=Decimal('1.00'),
date=timezone.now().date()
)









Ejemplo Real: Sistema de Transferencias






Caso de Uso: Transferencia entre Cuentas






CODE
from django.db import transaction
from decimal import Decimal

def transfer_between_accounts(
from_account: ForexAccount,
to_account: ForexAccount,
amount: Decimal,
date=None
) -> tuple[ForexTransaction, ForexTransaction]:
"""
Realiza una transferencia entre cuentas, manejando la conversión
de monedas si es necesario.
"""
if not date:
date = timezone.now().date()

with transaction.atomic():
# Obtener tipo de cambio si las monedas son diferentes
exchange_rate = None
if from_account.currency != to_account.currency:
exchange_rate = ExchangeRate.objects.get(
currency_from=from_account.currency,
currency_to=to_account.currency,
date=date
)

# Crear transacción de salida
outgoing = ForexTransaction.objects.create(
account=from_account,
type='OUT',
amount=amount,
date=date,
description=f'Transferencia a {to_account.code}'
)

# Calcular monto en moneda destino si es necesario
incoming_amount = amount
if exchange_rate:
incoming_amount = amount * exchange_rate.rate

# Crear transacción de ingreso
incoming = ForexTransaction.objects.create(
account=to_account,
type='IN',
amount=incoming_amount,
date=date,
description=f'Transferencia desde {from_account.code}'
)

return outgoing, incoming









Mejores Prácticas





  1. Validaciones de Seguridad


    • Usar decimal.Decimal para todos los cálculos monetarios








Mejores Prácticas (continuación)





  1. Validaciones de Seguridad


    • Usar decimal.Decimal para todos los cálculos monetarios

    • Implementar validaciones a nivel de modelo

    • Utilizar transacciones atómicas para operaciones críticas

    • Mantener un registro de auditoría






CODE
# models.py
class AuditMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
'auth.User',
on_delete=models.PROTECT,
related_name='%(class)s_created',
null=True
)
updated_by = models.ForeignKey(
'auth.User',
on_delete=models.PROTECT,
related_name='%(class)s_updated',
null=True
)

class Meta:
abstract = True

class ForexTransaction(AuditMixin, models.Model):
# ... (código anterior del modelo) ...

def save(self, *args, **kwargs):
if not self.pk: # Solo en creación
if not hasattr(self, 'created_by') and hasattr(self, '_current_user'):
self.created_by = self._current_user
if hasattr(self, '_current_user'):
self.updated_by = self._current_user
super().save(*args, **kwargs)








  1. Manejo de Errores


    • Implementar excepciones personalizadas

    • Logging detallado

    • Mensajes de error amigables en el admin






CODE
# exceptions.py
class ForexError(Exception):
"""Base exception for forex operations"""
pass

class ExchangeRateError(ForexError):
"""Raised when there's an issue with exchange rates"""
pass

class InsufficientFundsError(ForexError):
"""Raised when an account has insufficient funds"""
pass

# models.py
from django.core.exceptions import ValidationError
import logging

logger = logging.getLogger(__name__)

class ForexAccount(AuditMixin, models.Model):
def withdraw(self, amount: Decimal) -> None:
if self.balance < amount:
logger.error(
f"Insufficient funds in account {self.code}: "
f"balance={self.balance}, amount={amount}"
)
raise InsufficientFundsError(
f"Saldo insuficiente. Disponible: {self.balance}"
)
self.balance -= amount
self.save()








  1. Patrones de Diseño Recomendados


    • Repository Pattern para acceso a datos

    • Service Layer para lógica de negocio

    • Command Pattern para operaciones complejas






CODE
# services.py
class ForexService:
@staticmethod
def calculate_exchange_difference(
account: ForexAccount,
date_from: date,
date_to: date
) -> Decimal:
"""
Calcula la diferencia de cambio para una cuenta en un período
"""
initial_rate = ExchangeRate.objects.get(
currency_from=account.currency,
currency_to=Currency.objects.get(is_base=True),
date=date_from
)

final_rate = ExchangeRate.objects.get(
currency_from=account.currency,
currency_to=Currency.objects.get(is_base=True),
date=date_to
)

balance = account.balance
initial_value = balance * initial_rate.rate
final_value = balance * final_rate.rate

return final_value - initial_value

class ForexReportService:
@staticmethod
def get_position_report(date: date) -> dict:
"""
Genera reporte de posición de cambio
"""
base_currency = Currency.objects.get(is_base=True)
accounts = ForexAccount.objects.all()

position = {
'date': date,
'positions': [],
'total_base_currency': Decimal('0.00')
}

for account in accounts:
if account.currency != base_currency:
balance_base = account.get_balance_base_currency(date)
position['positions'].append({
'account': account.code,
'currency': account.currency.code,
'balance': account.balance,
'balance_base': balance_base
})
position['total_base_currency'] += balance_base

return position









Tests Adicionales






CODE
# tests.py
class ForexServiceTests(TestCase):
def setUp(self):
# ... (setup anterior) ...

def test_exchange_difference_calculation(self):
"""
Verifica el cálculo correcto de la diferencia de cambio
"""
# Crear tipos de cambio para diferentes fechas
date1 = timezone.now().date()
date2 = date1 + timedelta(days=1)

ExchangeRate.objects.create(
currency_from=self.usd,
currency_to=self.pen,
rate=Decimal('3.85'),
date=date1
)

ExchangeRate.objects.create(
currency_from=self.usd,
currency_to=self.pen,
rate=Decimal('3.90'),
date=date2
)

# Establecer saldo en la cuenta
self.account.balance = Decimal('1000.00')
self.account.save()

# Calcular diferencia de cambio
difference = ForexService.calculate_exchange_difference(
self.account,
date1,
date2
)

# La diferencia debería ser (3.90 - 3.85) * 1000 = 50
self.assertEqual(difference, Decimal('50.00'))

def test_position_report(self):
"""
Verifica la generación correcta del reporte de posición
"""
date = timezone.now().date()

# Crear algunas cuentas con saldos
ForexAccount.objects.create(
currency=self.usd,
code='1042',
name='Otra cuenta USD',
balance=Decimal('2000.00')
)

position = ForexReportService.get_position_report(date)

self.assertEqual(len(position['positions']), 2)
self.assertGreater(position['total_base_currency'], Decimal('0'))









Conclusión



Has aprendido a implementar un sistema robusto de control de cambios en Django que:




  1. Maneja tipos de cambio y diferencias cambiarias

  2. Implementa validaciones y seguridad

  3. Proporciona reportes de posición de cambio

  4. Es fácilmente extensible

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Contabilidad para Django Developers: Control de Cambios y Tipo de Cambio

Thematisch verwandte Begriffe: Contabilidad, para, Django, Developers · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...