38086-vm/accounting/signals.py
2026-02-08 06:12:20 +00:00

168 lines
5.3 KiB
Python

from django.utils import timezone
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from core.models import Sale, SalePayment, Purchase, PurchasePayment, Expense, SaleReturn, PurchaseReturn
from .models import Account, JournalEntry, JournalItem
from django.contrib.contenttypes.models import ContentType
from decimal import Decimal
def get_account(code):
try:
return Account.objects.get(code=code)
except Account.DoesNotExist:
return None
def create_journal_entry(obj, description, items):
"""
items: list of dicts {'account': Account, 'type': 'debit'/'credit', 'amount': Decimal}
"""
content_type = ContentType.objects.get_for_model(obj)
# Delete existing entry if any (to handle updates)
JournalEntry.objects.filter(content_type=content_type, object_id=obj.id).delete()
if not items:
return None
# Filter out items with 0 amount
valid_items = []
for item in items:
try:
# Ensure amount is Decimal
amount = Decimal(str(item['amount']))
if amount > 0:
item['amount'] = amount
valid_items.append(item)
except:
continue
if not valid_items:
return None
# Determine Entry Date
entry_date = timezone.now().date()
if hasattr(obj, 'date'):
entry_date = obj.date
elif hasattr(obj, 'payment_date'):
entry_date = obj.payment_date
elif hasattr(obj, 'created_at'):
entry_date = obj.created_at.date()
entry = JournalEntry.objects.create(
date=entry_date,
description=description,
content_type=content_type,
object_id=obj.id,
reference=f"{obj.__class__.__name__} #{obj.id}"
)
for item in valid_items:
JournalItem.objects.create(
entry=entry,
account=item['account'],
type=item['type'],
amount=item['amount']
)
return entry
@receiver(post_save, sender=Sale)
def sale_accounting_handler(sender, instance, created, **kwargs):
# Sale (Invoice) Entry
# Debit: Accounts Receivable (1200)
# Credit: Sales Revenue (4000)
# Credit: VAT Payable (2100) (if applicable)
ar_acc = get_account('1200')
sales_acc = get_account('4000')
if not ar_acc or not sales_acc:
return
items = [
{'account': ar_acc, 'type': 'debit', 'amount': instance.total_amount},
{'account': sales_acc, 'type': 'credit', 'amount': instance.total_amount},
]
create_journal_entry(instance, f"Sale Invoice #{instance.id}", items)
@receiver(post_save, sender=SalePayment)
def sale_payment_accounting_handler(sender, instance, created, **kwargs):
# Debit: Cash (1000)
# Credit: Accounts Receivable (1200)
cash_acc = get_account('1000')
ar_acc = get_account('1200')
if not cash_acc or not ar_acc:
return
items = [
{'account': cash_acc, 'type': 'debit', 'amount': instance.amount},
{'account': ar_acc, 'type': 'credit', 'amount': instance.amount},
]
create_journal_entry(instance, f"Payment for Sale #{instance.sale.id}", items)
@receiver(post_save, sender=Purchase)
def purchase_accounting_handler(sender, instance, created, **kwargs):
# Debit: Inventory (1300)
# Credit: Accounts Payable (2000)
inv_acc = get_account('1300')
ap_acc = get_account('2000')
if not inv_acc or not ap_acc:
return
items = [
{'account': inv_acc, 'type': 'debit', 'amount': instance.total_amount},
{'account': ap_acc, 'type': 'credit', 'amount': instance.total_amount},
]
create_journal_entry(instance, f"Purchase Invoice #{instance.id}", items)
@receiver(post_save, sender=PurchasePayment)
def purchase_payment_accounting_handler(sender, instance, created, **kwargs):
# Debit: Accounts Payable (2000)
# Credit: Cash (1000)
ap_acc = get_account('2000')
cash_acc = get_account('1000')
if not ap_acc or not cash_acc:
return
items = [
{'account': ap_acc, 'type': 'debit', 'amount': instance.amount},
{'account': cash_acc, 'type': 'credit', 'amount': instance.amount},
]
create_journal_entry(instance, f"Payment for Purchase #{instance.purchase.id}", items)
@receiver(post_save, sender=Expense)
def expense_accounting_handler(sender, instance, created, **kwargs):
# Debit: Expense Account
# Credit: Cash (1000)
expense_acc = instance.category.accounting_account or get_account('5400') # Default to General Expense
cash_acc = get_account('1000')
if not expense_acc or not cash_acc:
return
items = [
{'account': expense_acc, 'type': 'debit', 'amount': instance.amount},
{'account': cash_acc, 'type': 'credit', 'amount': instance.amount},
]
create_journal_entry(instance, f"Expense: {instance.category.name_en}", items)
@receiver(post_delete, sender=Sale)
@receiver(post_delete, sender=SalePayment)
@receiver(post_delete, sender=Purchase)
@receiver(post_delete, sender=PurchasePayment)
@receiver(post_delete, sender=Expense)
def delete_accounting_entry(sender, instance, **kwargs):
content_type = ContentType.objects.get_for_model(instance)
JournalEntry.objects.filter(content_type=content_type, object_id=instance.id).delete()