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 entry = JournalEntry.objects.create( date=getattr(obj, 'created_at', timezone.now()).date() if hasattr(obj, 'created_at') else timezone.now().date(), description=description, content_type=content_type, object_id=obj.id, reference=f"{obj.__class__.__name__} #{obj.id}" ) for item in items: if item['amount'] > 0: 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') vat_acc = get_account('2100') if not ar_acc or not sales_acc: return # Subtotal and VAT logic (assuming total_amount includes VAT for now as per Sale model simplicity) # Actually Sale model has total_amount and discount. # Let's assume total_amount is the final amount. 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()