حسابداری برای توسعه دهندگان جنگو: پیاده سازی کتاب های حسابداری

Summarize this content to 400 words in Persian Lang
مقدمه
به عنوان یک توسعه دهنده جنگو، آیا تا به حال با چالش پیاده سازی یک سیستم حسابداری مواجه شده اید و احساس کرده اید که تحت تأثیر اصطلاحات مالی قرار گرفته اید؟ شما تنها نیستید. حسابداری ممکن است دنیایی کاملاً متفاوت از برنامه نویسی به نظر برسد، اما در واقعیت، آنها در بسیاری از مفاهیم اساسی مشترک هستند.
در این آموزش نحوه پیادهسازی کتابهای اصلی حسابداری (ژورنال، دفتر کل و تراز آزمایشی) را با استفاده از Django Admin، مرتبط کردن مفاهیم حسابداری با پارادایمهای برنامهنویسی که قبلاً میدانید، یاد میگیریم. به عنوان مثال، یک ورودی مجله شبیه به یک تراکنش پایگاه داده است: هر دو باید یکپارچگی را حفظ کنند و اتمی باشند.
در پایان این آموزش، شما قادر خواهید بود یک سیستم حسابداری قوی را تنها با استفاده از Django Admin، با اعتبارسنجی خودکار و تست های واحد که یکپارچگی داده ها را تضمین می کند، پیاده سازی کنید.
پیش نیازها
مفاهیم کلیدی
حسابداری از دیدگاه توسعه دهنده
کتاب روزانه ← آن را به عنوان تاریخچه ارتکاب Git خود در نظر بگیرید
هر ورودی مانند یک تعهد است که تغییری را در صورت مالی ثبت می کند
باید تغییر ناپذیر باشد (مثل یک commit)
دارای مهر زمانی و ابرداده است (نویسنده، توضیحات)
دفتر کل ← شبیه به یک جمع کننده سیاهه بر اساس دسته
گروهبندی تراکنشها بر اساس «حساب» (مانند گروهبندی گزارشها بر اساس سرویس)
تعادل های به روز را حفظ می کند (مانند شمارنده در Redis)
تراز آزمایشی → معادل تغییر بررسی
بررسی کنید که سیستم در یک وضعیت سازگار است
باید قوانین ریاضی خاصی را رعایت کند (مانند اظهارات در آزمون ها)
پیاده سازی عملی
مدل های جنگو
from django.db import models
from django.core.exceptions import ValidationError
from django.db.models import Sum
from decimal import Decimal
import uuid
class Account(models.Model):
“””
Representa una cuenta contable.
Similar a una tabla en base de datos donde se agregan o restan valores.
“””
ACCOUNT_TYPES = [
(‘ASSET’, ‘Activo’),
(‘LIABILITY’, ‘Pasivo’),
(‘EQUITY’, ‘Patrimonio’),
(‘INCOME’, ‘Ingreso’),
(‘EXPENSE’, ‘Gasto’),
]
code = models.CharField(max_length=20, unique=True)
name = models.CharField(max_length=100)
type = models.CharField(max_length=10, choices=ACCOUNT_TYPES)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f”{self.code} – {self.name}”
def get_balance(self, end_date=None):
“””
Calcula el saldo de la cuenta hasta una fecha específica.
Similar a un query con filtro temporal.
“””
entries = JournalEntryLine.objects.filter(account=self)
if end_date:
entries = entries.filter(entry__date__lte=end_date)
debit_sum = entries.aggregate(Sum(‘debit’))[‘debit__sum’] or Decimal(‘0’)
credit_sum = entries.aggregate(Sum(‘credit’))[‘credit__sum’] or Decimal(‘0’)
if self.type in [‘ASSET’, ‘EXPENSE’]:
return debit_sum – credit_sum
return credit_sum – debit_sum
class Meta:
ordering = [‘code’]
class JournalEntry(models.Model):
“””
Representa un asiento contable.
Similar a una transacción en base de datos.
“””
reference = models.UUIDField(default=uuid.uuid4, editable=False)
date = models.DateField()
description = models.TextField()
is_posted = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(‘auth.User’, on_delete=models.PROTECT)
def clean(self):
“””
Validaciones a nivel de asiento.
Similar a validaciones de integridad en DB.
“””
if self.is_posted:
raise ValidationError(“No se puede modificar un asiento contabilizado”)
# Verificar balance entre débito y crédito
total_debit = sum(line.debit for line in self.lines.all())
total_credit = sum(line.credit for line in self.lines.all())
if total_debit != total_credit:
raise ValidationError(“El asiento no está balanceado”)
def post(self):
“””
Contabiliza el asiento.
Similar a un commit en una transacción.
“””
self.clean()
self.is_posted = True
self.save()
class Meta:
verbose_name_plural = “Journal Entries”
ordering = [‘-date’, ‘-created_at’]
class JournalEntryLine(models.Model):
“””
Representa una línea de asiento contable.
Similar a un detalle de transacción.
“””
entry = models.ForeignKey(
JournalEntry,
related_name=’lines’,
on_delete=models.CASCADE
)
account = models.ForeignKey(
Account,
on_delete=models.PROTECT
)
description = models.CharField(max_length=200)
debit = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0
)
credit = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0
)
def clean(self):
“””
Validaciones a nivel de línea.
Similar a check constraints en DB.
“””
if self.debit < 0 or self.credit < 0:
raise ValidationError(“Los montos 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”)
if self.debit == 0 and self.credit == 0:
raise ValidationError(“El monto debe ser mayor que cero”)
class Meta:
ordering = [‘id’]
class TrialBalance(models.Model):
“””
Balance de Comprobación.
Similar a un reporte de health check del sistema.
“””
date = models.DateField(unique=True)
is_closed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(‘auth.User’, on_delete=models.PROTECT)
def generate(self):
“””
Genera el balance de comprobación.
Similar a generar un reporte de estado del sistema.
“””
if self.is_closed:
raise ValidationError(“Este balance ya está cerrado”)
# Eliminar líneas existentes
self.lines.all().delete()
# Generar nuevas líneas
for account in Account.objects.filter(is_active=True):
balance = account.get_balance(self.date)
if balance != 0:
TrialBalanceLine.objects.create(
trial_balance=self,
account=account,
debit=max(balance, 0),
credit=max(-balance, 0)
)
def validate(self):
“””
Valida el balance de comprobación.
Similar a ejecutar test suite.
“””
total_debit = self.lines.aggregate(Sum(‘debit’))[‘debit__sum’] or 0
total_credit = self.lines.aggregate(Sum(‘credit’))[‘credit__sum’] or 0
if total_debit != total_credit:
raise ValidationError(
f”El balance no cuadra: Débito={total_debit}, Crédito={total_credit}”
)
class Meta:
ordering = [‘-date’]
class TrialBalanceLine(models.Model):
“””
Línea de Balance de Comprobación.
Similar a un resultado individual de test.
“””
trial_balance = models.ForeignKey(
TrialBalance,
related_name=’lines’,
on_delete=models.CASCADE
)
account = models.ForeignKey(Account, on_delete=models.PROTECT)
debit = models.DecimalField(max_digits=15, decimal_places=2, default=0)
credit = models.DecimalField(max_digits=15, decimal_places=2, default=0)
class Meta:
ordering = [‘account__code’]
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تنظیمات مدیریت
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from .models import Account, JournalEntry, JournalEntryLine, TrialBalance, TrialBalanceLine
@admin.register(Account)
class AccountAdmin(admin.ModelAdmin):
list_display = [‘code’, ‘name’, ‘type’, ‘get_balance_display’, ‘is_active’]
list_filter = [‘type’, ‘is_active’]
search_fields = [‘code’, ‘name’]
def get_balance_display(self, obj):
balance = obj.get_balance()
return f”{balance:,.2f}”
get_balance_display.short_description = ‘Balance’
class JournalEntryLineInline(admin.TabularInline):
model = JournalEntryLine
extra = 2
def get_readonly_fields(self, request, obj=None):
if obj and obj.is_posted:
return [‘account’, ‘description’, ‘debit’, ‘credit’]
return []
@admin.register(JournalEntry)
class JournalEntryAdmin(admin.ModelAdmin):
list_display = [‘reference’, ‘date’, ‘description’, ‘is_posted’, ‘created_by’]
list_filter = [‘is_posted’, ‘date’]
search_fields = [‘reference’, ‘description’]
readonly_fields = [‘reference’, ‘created_by’]
inlines = [JournalEntryLineInline]
def save_model(self, request, obj, form, change):
if not obj.pk:
obj.created_by = request.user
super().save_model(request, obj, form, change)
def get_readonly_fields(self, request, obj=None):
if obj and obj.is_posted:
return [‘reference’, ‘date’, ‘description’, ‘is_posted’, ‘created_by’]
return [‘reference’, ‘created_by’]
class TrialBalanceLineInline(admin.TabularInline):
model = TrialBalanceLine
extra = 0
readonly_fields = [‘account’, ‘debit’, ‘credit’]
can_delete = False
def has_add_permission(self, request, obj=None):
return False
@admin.register(TrialBalance)
class TrialBalanceAdmin(admin.ModelAdmin):
list_display = [‘date’, ‘is_closed’, ‘created_by’, ‘created_at’]
readonly_fields = [‘created_by’, ‘created_at’]
inlines = [TrialBalanceLineInline]
def save_model(self, request, obj, form, change):
if not obj.pk:
obj.created_by = request.user
super().save_model(request, obj, form, change)
def response_add(self, request, obj):
obj.generate()
obj.validate()
return super().response_add(request, obj)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست های واحد
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from decimal import Decimal
from .models import Account, JournalEntry, JournalEntryLine, TrialBalance
from datetime import date
class AccountingTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username=’testuser’,
password=’testpass’
)
# Crear cuentas de prueba
self.cash = Account.objects.create(
code=’1000′,
name=’Cash’,
type=’ASSET’
)
self.bank = Account.objects.create(
code=’1001′,
name=’Bank’,
type=’ASSET’
)
self.revenue = Account.objects.create(
code=’4000′,
name=’Revenue’,
type=’INCOME’
)
def test_journal_entry_balance(self):
“””Verifica que los asientos contables estén balanceados”””
entry = JournalEntry.objects.create(
date=date.today(),
description=’Test Entry’,
created_by=self.user
)
# Crear líneas desbalanceadas
JournalEntryLine.objects.create(
entry=entry,
account=self.cash,
description=’Debit line’,
debit=Decimal(‘100.00′)
)
JournalEntryLine.objects.create(
entry=entry,
account=self.revenue,
description=’Credit line’,
credit=Decimal(‘90.00′)
)
# Intentar contabilizar debería fallar
with self.assertRaises(ValidationError):
entry.post()
def test_trial_balance(self):
“””Verifica la generación del balance de comprobación”””
# Crear y contabilizar un asiento
entry = JournalEntry.objects.create(
date=date.today(),
description=’Test Entry’,
created_by=self.user
)
JournalEntryLine.objects.create(
entry=entry,
account=self.cash,
description=’Debit line’,
debit=Decimal(‘100.00′)
)
JournalEntryLine.objects.create(
entry=entry,
account=self.revenue,
description=’Credit line’,
credit=Decimal(‘100.00’)
)
entry.post()
# Generar balance de comprobación
trial_balance = TrialBalance.objects.create(
date=date.today(),
created_by=self.user
)
trial_balance.generate()
# Verificar saldos
self.assertEqual(
trial_balance.lines.aggregate(Sum(‘debit’))[‘debit__sum’],
trial_balance.lines.aggregate(Sum(‘credit’))[‘credit__sum’]
)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
مثال واقعی: سیستم انتقال بین حساب ها
حواله بانکی ایجاد کنید
def create_bank_transfer(
date,
amount,
from_account,
to_account,
description,
user
):
“””
Crea un asiento contable para una transferencia bancaria.
“””
entry = JournalEntry.objects.create(
date=date,
description=description,
created_by=user
)
# Crear línea de débito (cuenta destino)
JournalEntryLine.objects.create(
entry=entry,
account=to_account,
description=f”Transferencia recibida de {from_account.name}”,
debit=amount
)
# Crear línea de crédito (cuenta origen)
JournalEntryLine.objects.create(
entry=entry,
account=from_account,
description=f”Transferencia enviada a {to_account.name}”,
credit=amount
)
# Contabilizar el asiento
entry.post()
return entry
# Ejemplo de uso:
from decimal import Decimal
from datetime import date
transfer = create_bank_transfer(
date=date.today(),
amount=Decimal(‘1000.00′),
from_account=Account.objects.get(code=’1001′), # Cuenta Banco
to_account=Account.objects.get(code=’1000’), # Cuenta Caja
description=”Transferencia para gastos operativos”,
user=request.user
)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
بهترین شیوه ها
1. اعتبارسنجی امنیتی
from django.contrib.admin import ModelAdmin
from django.core.exceptions import PermissionDenied
class JournalEntryAdmin(ModelAdmin):
def has_delete_permission(self, request, obj=None):
# Prevenir eliminación de asientos contabilizados
if obj and obj.is_posted:
return False
return super().has_delete_permission(request, obj)
def save_model(self, request, obj, form, change):
# Verificar permisos especiales para contabilizar
if ‘is_posted’ in form.changed_data:
if not request.user.has_perm(‘accounting.post_journal_entry’):
raise PermissionDenied(“No tienes permiso para contabilizar asientos”)
super().save_model(request, obj, form, change)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
2. رسیدگی به خطا
class AccountingError(Exception):
“””Base exception for accounting errors”””
pass
class UnbalancedEntryError(AccountingError):
“””Raised when a journal entry is not balanced”””
pass
class PostedEntryError(AccountingError):
“””Raised when trying to modify a posted entry”””
pass
class JournalEntry(models.Model):
# … otros campos …
def post(self):
try:
# Validar balance
total_debit = self.lines.aggregate(Sum(‘debit’))[‘debit__sum’] or 0
total_credit = self.lines.aggregate(Sum(‘credit’))[‘credit__sum’] or 0
if total_debit != total_credit:
raise UnbalancedEntryError(
f”Débito ({total_debit}) != Crédito ({total_credit})”
)
# Validar que no esté contabilizado
if self.is_posted:
raise PostedEntryError(“El asiento ya está contabilizado”)
# Contabilizar
self.is_posted = True
self.save()
except AccountingError as e:
# Log del error
logger.error(f”Error al contabilizar asiento {self.reference}: {str(e)}”)
raise
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
3. الگوهای طراحی توصیه شده
الگوی فرمان برای عملیات حسابداری
from abc import ABC, abstractmethod
from decimal import Decimal
from typing import List
class AccountingCommand(ABC):
@abstractmethod
def execute(self) -> JournalEntry:
pass
class TransferCommand(AccountingCommand):
def __init__(
self,
date: date,
amount: Decimal,
from_account: Account,
to_account: Account,
description: str,
user: User
):
self.date = date
self.amount = amount
self.from_account = from_account
self.to_account = to_account
self.description = description
self.user = user
def execute(self) -> JournalEntry:
return create_bank_transfer(
self.date,
self.amount,
self.from_account,
self.to_account,
self.description,
self.user
)
# Uso del Command Pattern
transfer_cmd = TransferCommand(
date=date.today(),
amount=Decimal(‘1000.00′),
from_account=bank_account,
to_account=cash_account,
description=”Transferencia operativa”,
user=current_user
)
journal_entry = transfer_cmd.execute()
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پرس و جوهای مفید
1. موجودی بر اساس نوع حساب
from django.db.models import Sum, Case, When, F
def get_account_type_balances(end_date=None):
“””
Obtiene los saldos agrupados por tipo de cuenta
“””
query = Account.objects.all()
if end_date:
lines = JournalEntryLine.objects.filter(
entry__date__lte=end_date,
entry__is_posted=True
)
else:
lines = JournalEntryLine.objects.filter(entry__is_posted=True)
balances = query.annotate(
total_debit=Sum(
Case(
When(
journalentryline__in=lines,
then=’journalentryline__debit’
),
default=0
)
),
total_credit=Sum(
Case(
When(
journalentryline__in=lines,
then=’journalentryline__credit’
),
default=0
)
),
balance=Case(
When(
type__in=[‘ASSET’, ‘EXPENSE’],
then=F(‘total_debit’) – F(‘total_credit’)
),
default=F(‘total_credit’) – F(‘total_debit’)
)
).values(‘type’).annotate(
total_balance=Sum(‘balance’)
)
return balances
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
2. دفتر کل
def get_ledger(account, start_date=None, end_date=None):
“””
Obtiene el libro mayor para una cuenta específica
“””
lines = JournalEntryLine.objects.filter(
account=account,
entry__is_posted=True
).select_related(‘entry’)
if start_date:
lines = lines.filter(entry__date__gte=start_date)
if end_date:
lines = lines.filter(entry__date__lte=end_date)
return lines.order_by(‘entry__date’, ‘entry__id’)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
نتیجه گیری
در این آموزش، ما یک سیستم حسابداری قوی با استفاده از Django Admin ساخته ایم که از اصول برنامه نویسی آشنا برای درک مفاهیم حسابداری استفاده می کند. نکات کلیدی عبارتند از:
حسابداری مشابه سیستم ثبت معاملات است
اعتبارسنجی برای حفظ یکپارچگی داده ها بسیار مهم است
الگوی Command به ما کمک می کند تا عملیات حسابداری پیچیده را محصور کنیم
مقدمه
به عنوان یک توسعه دهنده جنگو، آیا تا به حال با چالش پیاده سازی یک سیستم حسابداری مواجه شده اید و احساس کرده اید که تحت تأثیر اصطلاحات مالی قرار گرفته اید؟ شما تنها نیستید. حسابداری ممکن است دنیایی کاملاً متفاوت از برنامه نویسی به نظر برسد، اما در واقعیت، آنها در بسیاری از مفاهیم اساسی مشترک هستند.
در این آموزش نحوه پیادهسازی کتابهای اصلی حسابداری (ژورنال، دفتر کل و تراز آزمایشی) را با استفاده از Django Admin، مرتبط کردن مفاهیم حسابداری با پارادایمهای برنامهنویسی که قبلاً میدانید، یاد میگیریم. به عنوان مثال، یک ورودی مجله شبیه به یک تراکنش پایگاه داده است: هر دو باید یکپارچگی را حفظ کنند و اتمی باشند.
در پایان این آموزش، شما قادر خواهید بود یک سیستم حسابداری قوی را تنها با استفاده از Django Admin، با اعتبارسنجی خودکار و تست های واحد که یکپارچگی داده ها را تضمین می کند، پیاده سازی کنید.
پیش نیازها
مفاهیم کلیدی
حسابداری از دیدگاه توسعه دهنده
-
کتاب روزانه ← آن را به عنوان تاریخچه ارتکاب Git خود در نظر بگیرید
- هر ورودی مانند یک تعهد است که تغییری را در صورت مالی ثبت می کند
- باید تغییر ناپذیر باشد (مثل یک commit)
- دارای مهر زمانی و ابرداده است (نویسنده، توضیحات)
-
دفتر کل ← شبیه به یک جمع کننده سیاهه بر اساس دسته
- گروهبندی تراکنشها بر اساس «حساب» (مانند گروهبندی گزارشها بر اساس سرویس)
- تعادل های به روز را حفظ می کند (مانند شمارنده در Redis)
-
تراز آزمایشی → معادل تغییر بررسی
- بررسی کنید که سیستم در یک وضعیت سازگار است
- باید قوانین ریاضی خاصی را رعایت کند (مانند اظهارات در آزمون ها)
پیاده سازی عملی
مدل های جنگو
from django.db import models
from django.core.exceptions import ValidationError
from django.db.models import Sum
from decimal import Decimal
import uuid
class Account(models.Model):
"""
Representa una cuenta contable.
Similar a una tabla en base de datos donde se agregan o restan valores.
"""
ACCOUNT_TYPES = [
('ASSET', 'Activo'),
('LIABILITY', 'Pasivo'),
('EQUITY', 'Patrimonio'),
('INCOME', 'Ingreso'),
('EXPENSE', 'Gasto'),
]
code = models.CharField(max_length=20, unique=True)
name = models.CharField(max_length=100)
type = models.CharField(max_length=10, choices=ACCOUNT_TYPES)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.code} - {self.name}"
def get_balance(self, end_date=None):
"""
Calcula el saldo de la cuenta hasta una fecha específica.
Similar a un query con filtro temporal.
"""
entries = JournalEntryLine.objects.filter(account=self)
if end_date:
entries = entries.filter(entry__date__lte=end_date)
debit_sum = entries.aggregate(Sum('debit'))['debit__sum'] or Decimal('0')
credit_sum = entries.aggregate(Sum('credit'))['credit__sum'] or Decimal('0')
if self.type in ['ASSET', 'EXPENSE']:
return debit_sum - credit_sum
return credit_sum - debit_sum
class Meta:
ordering = ['code']
class JournalEntry(models.Model):
"""
Representa un asiento contable.
Similar a una transacción en base de datos.
"""
reference = models.UUIDField(default=uuid.uuid4, editable=False)
date = models.DateField()
description = models.TextField()
is_posted = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey('auth.User', on_delete=models.PROTECT)
def clean(self):
"""
Validaciones a nivel de asiento.
Similar a validaciones de integridad en DB.
"""
if self.is_posted:
raise ValidationError("No se puede modificar un asiento contabilizado")
# Verificar balance entre débito y crédito
total_debit = sum(line.debit for line in self.lines.all())
total_credit = sum(line.credit for line in self.lines.all())
if total_debit != total_credit:
raise ValidationError("El asiento no está balanceado")
def post(self):
"""
Contabiliza el asiento.
Similar a un commit en una transacción.
"""
self.clean()
self.is_posted = True
self.save()
class Meta:
verbose_name_plural = "Journal Entries"
ordering = ['-date', '-created_at']
class JournalEntryLine(models.Model):
"""
Representa una línea de asiento contable.
Similar a un detalle de transacción.
"""
entry = models.ForeignKey(
JournalEntry,
related_name='lines',
on_delete=models.CASCADE
)
account = models.ForeignKey(
Account,
on_delete=models.PROTECT
)
description = models.CharField(max_length=200)
debit = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0
)
credit = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0
)
def clean(self):
"""
Validaciones a nivel de línea.
Similar a check constraints en DB.
"""
if self.debit < 0 or self.credit < 0:
raise ValidationError("Los montos 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")
if self.debit == 0 and self.credit == 0:
raise ValidationError("El monto debe ser mayor que cero")
class Meta:
ordering = ['id']
class TrialBalance(models.Model):
"""
Balance de Comprobación.
Similar a un reporte de health check del sistema.
"""
date = models.DateField(unique=True)
is_closed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey('auth.User', on_delete=models.PROTECT)
def generate(self):
"""
Genera el balance de comprobación.
Similar a generar un reporte de estado del sistema.
"""
if self.is_closed:
raise ValidationError("Este balance ya está cerrado")
# Eliminar líneas existentes
self.lines.all().delete()
# Generar nuevas líneas
for account in Account.objects.filter(is_active=True):
balance = account.get_balance(self.date)
if balance != 0:
TrialBalanceLine.objects.create(
trial_balance=self,
account=account,
debit=max(balance, 0),
credit=max(-balance, 0)
)
def validate(self):
"""
Valida el balance de comprobación.
Similar a ejecutar test suite.
"""
total_debit = self.lines.aggregate(Sum('debit'))['debit__sum'] or 0
total_credit = self.lines.aggregate(Sum('credit'))['credit__sum'] or 0
if total_debit != total_credit:
raise ValidationError(
f"El balance no cuadra: Débito={total_debit}, Crédito={total_credit}"
)
class Meta:
ordering = ['-date']
class TrialBalanceLine(models.Model):
"""
Línea de Balance de Comprobación.
Similar a un resultado individual de test.
"""
trial_balance = models.ForeignKey(
TrialBalance,
related_name='lines',
on_delete=models.CASCADE
)
account = models.ForeignKey(Account, on_delete=models.PROTECT)
debit = models.DecimalField(max_digits=15, decimal_places=2, default=0)
credit = models.DecimalField(max_digits=15, decimal_places=2, default=0)
class Meta:
ordering = ['account__code']
تنظیمات مدیریت
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from .models import Account, JournalEntry, JournalEntryLine, TrialBalance, TrialBalanceLine
@admin.register(Account)
class AccountAdmin(admin.ModelAdmin):
list_display = ['code', 'name', 'type', 'get_balance_display', 'is_active']
list_filter = ['type', 'is_active']
search_fields = ['code', 'name']
def get_balance_display(self, obj):
balance = obj.get_balance()
return f"{balance:,.2f}"
get_balance_display.short_description = 'Balance'
class JournalEntryLineInline(admin.TabularInline):
model = JournalEntryLine
extra = 2
def get_readonly_fields(self, request, obj=None):
if obj and obj.is_posted:
return ['account', 'description', 'debit', 'credit']
return []
@admin.register(JournalEntry)
class JournalEntryAdmin(admin.ModelAdmin):
list_display = ['reference', 'date', 'description', 'is_posted', 'created_by']
list_filter = ['is_posted', 'date']
search_fields = ['reference', 'description']
readonly_fields = ['reference', 'created_by']
inlines = [JournalEntryLineInline]
def save_model(self, request, obj, form, change):
if not obj.pk:
obj.created_by = request.user
super().save_model(request, obj, form, change)
def get_readonly_fields(self, request, obj=None):
if obj and obj.is_posted:
return ['reference', 'date', 'description', 'is_posted', 'created_by']
return ['reference', 'created_by']
class TrialBalanceLineInline(admin.TabularInline):
model = TrialBalanceLine
extra = 0
readonly_fields = ['account', 'debit', 'credit']
can_delete = False
def has_add_permission(self, request, obj=None):
return False
@admin.register(TrialBalance)
class TrialBalanceAdmin(admin.ModelAdmin):
list_display = ['date', 'is_closed', 'created_by', 'created_at']
readonly_fields = ['created_by', 'created_at']
inlines = [TrialBalanceLineInline]
def save_model(self, request, obj, form, change):
if not obj.pk:
obj.created_by = request.user
super().save_model(request, obj, form, change)
def response_add(self, request, obj):
obj.generate()
obj.validate()
return super().response_add(request, obj)
تست های واحد
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from decimal import Decimal
from .models import Account, JournalEntry, JournalEntryLine, TrialBalance
from datetime import date
class AccountingTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass'
)
# Crear cuentas de prueba
self.cash = Account.objects.create(
code='1000',
name='Cash',
type='ASSET'
)
self.bank = Account.objects.create(
code='1001',
name='Bank',
type='ASSET'
)
self.revenue = Account.objects.create(
code='4000',
name='Revenue',
type='INCOME'
)
def test_journal_entry_balance(self):
"""Verifica que los asientos contables estén balanceados"""
entry = JournalEntry.objects.create(
date=date.today(),
description='Test Entry',
created_by=self.user
)
# Crear líneas desbalanceadas
JournalEntryLine.objects.create(
entry=entry,
account=self.cash,
description='Debit line',
debit=Decimal('100.00')
)
JournalEntryLine.objects.create(
entry=entry,
account=self.revenue,
description='Credit line',
credit=Decimal('90.00')
)
# Intentar contabilizar debería fallar
with self.assertRaises(ValidationError):
entry.post()
def test_trial_balance(self):
"""Verifica la generación del balance de comprobación"""
# Crear y contabilizar un asiento
entry = JournalEntry.objects.create(
date=date.today(),
description='Test Entry',
created_by=self.user
)
JournalEntryLine.objects.create(
entry=entry,
account=self.cash,
description='Debit line',
debit=Decimal('100.00')
)
JournalEntryLine.objects.create(
entry=entry,
account=self.revenue,
description='Credit line',
credit=Decimal('100.00')
)
entry.post()
# Generar balance de comprobación
trial_balance = TrialBalance.objects.create(
date=date.today(),
created_by=self.user
)
trial_balance.generate()
# Verificar saldos
self.assertEqual(
trial_balance.lines.aggregate(Sum('debit'))['debit__sum'],
trial_balance.lines.aggregate(Sum('credit'))['credit__sum']
)
مثال واقعی: سیستم انتقال بین حساب ها
حواله بانکی ایجاد کنید
def create_bank_transfer(
date,
amount,
from_account,
to_account,
description,
user
):
"""
Crea un asiento contable para una transferencia bancaria.
"""
entry = JournalEntry.objects.create(
date=date,
description=description,
created_by=user
)
# Crear línea de débito (cuenta destino)
JournalEntryLine.objects.create(
entry=entry,
account=to_account,
description=f"Transferencia recibida de {from_account.name}",
debit=amount
)
# Crear línea de crédito (cuenta origen)
JournalEntryLine.objects.create(
entry=entry,
account=from_account,
description=f"Transferencia enviada a {to_account.name}",
credit=amount
)
# Contabilizar el asiento
entry.post()
return entry
# Ejemplo de uso:
from decimal import Decimal
from datetime import date
transfer = create_bank_transfer(
date=date.today(),
amount=Decimal('1000.00'),
from_account=Account.objects.get(code='1001'), # Cuenta Banco
to_account=Account.objects.get(code='1000'), # Cuenta Caja
description="Transferencia para gastos operativos",
user=request.user
)
بهترین شیوه ها
1. اعتبارسنجی امنیتی
from django.contrib.admin import ModelAdmin
from django.core.exceptions import PermissionDenied
class JournalEntryAdmin(ModelAdmin):
def has_delete_permission(self, request, obj=None):
# Prevenir eliminación de asientos contabilizados
if obj and obj.is_posted:
return False
return super().has_delete_permission(request, obj)
def save_model(self, request, obj, form, change):
# Verificar permisos especiales para contabilizar
if 'is_posted' in form.changed_data:
if not request.user.has_perm('accounting.post_journal_entry'):
raise PermissionDenied("No tienes permiso para contabilizar asientos")
super().save_model(request, obj, form, change)
2. رسیدگی به خطا
class AccountingError(Exception):
"""Base exception for accounting errors"""
pass
class UnbalancedEntryError(AccountingError):
"""Raised when a journal entry is not balanced"""
pass
class PostedEntryError(AccountingError):
"""Raised when trying to modify a posted entry"""
pass
class JournalEntry(models.Model):
# ... otros campos ...
def post(self):
try:
# Validar balance
total_debit = self.lines.aggregate(Sum('debit'))['debit__sum'] or 0
total_credit = self.lines.aggregate(Sum('credit'))['credit__sum'] or 0
if total_debit != total_credit:
raise UnbalancedEntryError(
f"Débito ({total_debit}) != Crédito ({total_credit})"
)
# Validar que no esté contabilizado
if self.is_posted:
raise PostedEntryError("El asiento ya está contabilizado")
# Contabilizar
self.is_posted = True
self.save()
except AccountingError as e:
# Log del error
logger.error(f"Error al contabilizar asiento {self.reference}: {str(e)}")
raise
3. الگوهای طراحی توصیه شده
الگوی فرمان برای عملیات حسابداری
from abc import ABC, abstractmethod
from decimal import Decimal
from typing import List
class AccountingCommand(ABC):
@abstractmethod
def execute(self) -> JournalEntry:
pass
class TransferCommand(AccountingCommand):
def __init__(
self,
date: date,
amount: Decimal,
from_account: Account,
to_account: Account,
description: str,
user: User
):
self.date = date
self.amount = amount
self.from_account = from_account
self.to_account = to_account
self.description = description
self.user = user
def execute(self) -> JournalEntry:
return create_bank_transfer(
self.date,
self.amount,
self.from_account,
self.to_account,
self.description,
self.user
)
# Uso del Command Pattern
transfer_cmd = TransferCommand(
date=date.today(),
amount=Decimal('1000.00'),
from_account=bank_account,
to_account=cash_account,
description="Transferencia operativa",
user=current_user
)
journal_entry = transfer_cmd.execute()
پرس و جوهای مفید
1. موجودی بر اساس نوع حساب
from django.db.models import Sum, Case, When, F
def get_account_type_balances(end_date=None):
"""
Obtiene los saldos agrupados por tipo de cuenta
"""
query = Account.objects.all()
if end_date:
lines = JournalEntryLine.objects.filter(
entry__date__lte=end_date,
entry__is_posted=True
)
else:
lines = JournalEntryLine.objects.filter(entry__is_posted=True)
balances = query.annotate(
total_debit=Sum(
Case(
When(
journalentryline__in=lines,
then='journalentryline__debit'
),
default=0
)
),
total_credit=Sum(
Case(
When(
journalentryline__in=lines,
then='journalentryline__credit'
),
default=0
)
),
balance=Case(
When(
type__in=['ASSET', 'EXPENSE'],
then=F('total_debit') - F('total_credit')
),
default=F('total_credit') - F('total_debit')
)
).values('type').annotate(
total_balance=Sum('balance')
)
return balances
2. دفتر کل
def get_ledger(account, start_date=None, end_date=None):
"""
Obtiene el libro mayor para una cuenta específica
"""
lines = JournalEntryLine.objects.filter(
account=account,
entry__is_posted=True
).select_related('entry')
if start_date:
lines = lines.filter(entry__date__gte=start_date)
if end_date:
lines = lines.filter(entry__date__lte=end_date)
return lines.order_by('entry__date', 'entry__id')
نتیجه گیری
در این آموزش، ما یک سیستم حسابداری قوی با استفاده از Django Admin ساخته ایم که از اصول برنامه نویسی آشنا برای درک مفاهیم حسابداری استفاده می کند. نکات کلیدی عبارتند از:
- حسابداری مشابه سیستم ثبت معاملات است
- اعتبارسنجی برای حفظ یکپارچگی داده ها بسیار مهم است
- الگوی Command به ما کمک می کند تا عملیات حسابداری پیچیده را محصور کنیم