diff --git a/accounting/__init__.py b/accounting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounting/__pycache__/__init__.cpython-311.pyc b/accounting/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a3bd12c Binary files /dev/null and b/accounting/__pycache__/__init__.cpython-311.pyc differ diff --git a/accounting/__pycache__/admin.cpython-311.pyc b/accounting/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000..6acedcb Binary files /dev/null and b/accounting/__pycache__/admin.cpython-311.pyc differ diff --git a/accounting/__pycache__/apps.cpython-311.pyc b/accounting/__pycache__/apps.cpython-311.pyc new file mode 100644 index 0000000..f7a4f42 Binary files /dev/null and b/accounting/__pycache__/apps.cpython-311.pyc differ diff --git a/accounting/__pycache__/models.cpython-311.pyc b/accounting/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..8b3540f Binary files /dev/null and b/accounting/__pycache__/models.cpython-311.pyc differ diff --git a/accounting/__pycache__/signals.cpython-311.pyc b/accounting/__pycache__/signals.cpython-311.pyc new file mode 100644 index 0000000..bd9edfe Binary files /dev/null and b/accounting/__pycache__/signals.cpython-311.pyc differ diff --git a/accounting/__pycache__/urls.cpython-311.pyc b/accounting/__pycache__/urls.cpython-311.pyc new file mode 100644 index 0000000..282fef8 Binary files /dev/null and b/accounting/__pycache__/urls.cpython-311.pyc differ diff --git a/accounting/__pycache__/views.cpython-311.pyc b/accounting/__pycache__/views.cpython-311.pyc new file mode 100644 index 0000000..be7c240 Binary files /dev/null and b/accounting/__pycache__/views.cpython-311.pyc differ diff --git a/accounting/admin.py b/accounting/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/accounting/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/accounting/apps.py b/accounting/apps.py new file mode 100644 index 0000000..ed61952 --- /dev/null +++ b/accounting/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + +class AccountingConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounting' + verbose_name = _('Accounting') + + def ready(self): + import accounting.signals \ No newline at end of file diff --git a/accounting/management/__init__.py b/accounting/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounting/management/__pycache__/__init__.cpython-311.pyc b/accounting/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1901e74 Binary files /dev/null and b/accounting/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/accounting/management/commands/__init__.py b/accounting/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounting/management/commands/__pycache__/__init__.cpython-311.pyc b/accounting/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..89fee31 Binary files /dev/null and b/accounting/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/accounting/management/commands/__pycache__/setup_accounts.cpython-311.pyc b/accounting/management/commands/__pycache__/setup_accounts.cpython-311.pyc new file mode 100644 index 0000000..8706e38 Binary files /dev/null and b/accounting/management/commands/__pycache__/setup_accounts.cpython-311.pyc differ diff --git a/accounting/management/commands/setup_accounts.py b/accounting/management/commands/setup_accounts.py new file mode 100644 index 0000000..efd6b0b --- /dev/null +++ b/accounting/management/commands/setup_accounts.py @@ -0,0 +1,47 @@ +from django.core.management.base import BaseCommand +from accounting.models import Account + +class Command(BaseCommand): + help = 'Setup default Chart of Accounts' + + def handle(self, *args, **options): + default_accounts = [ + # ASSETS + {'code': '1000', 'name_en': 'Cash', 'name_ar': 'النقد', 'account_type': 'asset'}, + {'code': '1010', 'name_en': 'Bank', 'name_ar': 'البنك', 'account_type': 'asset'}, + {'code': '1200', 'name_en': 'Accounts Receivable', 'name_ar': 'الذمم المدينة', 'account_type': 'asset'}, + {'code': '1300', 'name_en': 'Inventory', 'name_ar': 'المخزون', 'account_type': 'asset'}, + + # LIABILITIES + {'code': '2000', 'name_en': 'Accounts Payable', 'name_ar': 'الذمم الدائنة', 'account_type': 'liability'}, + {'code': '2100', 'name_en': 'VAT Payable', 'name_ar': 'ضريبة القيمة المضافة المستحقة', 'account_type': 'liability'}, + + # EQUITY + {'code': '3000', 'name_en': 'Owner Equity', 'name_ar': 'رأس المال', 'account_type': 'equity'}, + {'code': '3100', 'name_en': 'Retained Earnings', 'name_ar': 'الأرباح المحتجزة', 'account_type': 'equity'}, + + # INCOME + {'code': '4000', 'name_en': 'Sales Revenue', 'name_ar': 'إيرادات المبيعات', 'account_type': 'income'}, + {'code': '4100', 'name_en': 'Other Income', 'name_ar': 'إيرادات أخرى', 'account_type': 'income'}, + + # EXPENSES + {'code': '5000', 'name_en': 'Cost of Goods Sold', 'name_ar': 'تكلفة البضائع المباعة', 'account_type': 'expense'}, + {'code': '5100', 'name_en': 'Salaries Expense', 'name_ar': 'مصاريف الرواتب', 'account_type': 'expense'}, + {'code': '5200', 'name_en': 'Rent Expense', 'name_ar': 'مصاريف الإيجار', 'account_type': 'expense'}, + {'code': '5300', 'name_en': 'Utility Expense', 'name_ar': 'مصاريف المرافق', 'account_type': 'expense'}, + {'code': '5400', 'name_en': 'General Expense', 'name_ar': 'مصاريف عامة', 'account_type': 'expense'}, + ] + + for acc_data in default_accounts: + account, created = Account.objects.get_or_create( + code=acc_data['code'], + defaults={ + 'name_en': acc_data['name_en'], + 'name_ar': acc_data['name_ar'], + 'account_type': acc_data['account_type'] + } + ) + if created: + self.stdout.write(self.style.SUCCESS(f'Created account: {account.name_en}')) + else: + self.stdout.write(f'Account already exists: {account.name_en}') diff --git a/accounting/migrations/0001_initial.py b/accounting/migrations/0001_initial.py new file mode 100644 index 0000000..41d591a --- /dev/null +++ b/accounting/migrations/0001_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2.7 on 2026-02-03 03:14 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=20, unique=True, verbose_name='Account Code')), + ('name_en', models.CharField(max_length=100, verbose_name='Name (English)')), + ('name_ar', models.CharField(max_length=100, verbose_name='Name (Arabic)')), + ('account_type', models.CharField(choices=[('asset', 'Asset'), ('liability', 'Liability'), ('equity', 'Equity'), ('income', 'Income'), ('expense', 'Expense')], max_length=20, verbose_name='Account Type')), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='JournalEntry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(default=django.utils.timezone.now, verbose_name='Date')), + ('description', models.TextField(verbose_name='Description')), + ('reference', models.CharField(blank=True, max_length=100, verbose_name='Reference')), + ('object_id', models.PositiveIntegerField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ], + options={ + 'verbose_name_plural': 'Journal Entries', + }, + ), + migrations.CreateModel( + name='JournalItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('debit', 'Debit'), ('credit', 'Credit')], max_length=10, verbose_name='Type')), + ('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')), + ('notes', models.CharField(blank=True, max_length=255, verbose_name='Notes')), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='journal_items', to='accounting.account')), + ('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='accounting.journalentry')), + ], + ), + ] diff --git a/accounting/migrations/__init__.py b/accounting/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounting/migrations/__pycache__/0001_initial.cpython-311.pyc b/accounting/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..6fd60ef Binary files /dev/null and b/accounting/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/accounting/migrations/__pycache__/__init__.cpython-311.pyc b/accounting/migrations/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..d94ad5b Binary files /dev/null and b/accounting/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/accounting/models.py b/accounting/models.py new file mode 100644 index 0000000..d41bd79 --- /dev/null +++ b/accounting/models.py @@ -0,0 +1,73 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils import timezone +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType + +class Account(models.Model): + ACCOUNT_TYPES = [ + ('asset', _('Asset')), + ('liability', _('Liability')), + ('equity', _('Equity')), + ('income', _('Income')), + ('expense', _('Expense')), + ] + + code = models.CharField(_("Account Code"), max_length=20, unique=True) + name_en = models.CharField(_("Name (English)"), max_length=100) + name_ar = models.CharField(_("Name (Arabic)"), max_length=100) + account_type = models.CharField(_("Account Type"), max_length=20, choices=ACCOUNT_TYPES) + description = models.TextField(_("Description"), blank=True) + is_active = models.BooleanField(_("Is Active"), default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.code} - {self.name_en} / {self.name_ar}" + + @property + def balance(self): + # Calculate balance: Sum(debit) - Sum(credit) + items = self.journal_items.all() + debits = items.filter(type='debit').aggregate(total=models.Sum('amount'))['total'] or 0 + credits = items.filter(type='credit').aggregate(total=models.Sum('amount'))['total'] or 0 + + # Standard balances: + # Assets/Expenses: Debit - Credit + # Liabilities/Equity/Income: Credit - Debit + if self.account_type in ['asset', 'expense']: + return debits - credits + else: + return credits - debits + +class JournalEntry(models.Model): + date = models.DateField(_("Date"), default=timezone.now) + description = models.TextField(_("Description")) + reference = models.CharField(_("Reference"), max_length=100, blank=True) + + # Generic relationship to the source document (Sale, Purchase, Expense, etc.) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) + object_id = models.PositiveIntegerField(null=True, blank=True) + content_object = GenericForeignKey('content_type', 'object_id') + + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Entry {self.id} - {self.date} ({self.description[:30]})" + + class Meta: + verbose_name_plural = _("Journal Entries") + +class JournalItem(models.Model): + TYPE_CHOICES = [ + ('debit', _('Debit')), + ('credit', _('Credit')), + ] + + entry = models.ForeignKey(JournalEntry, on_delete=models.CASCADE, related_name="items") + account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="journal_items") + type = models.CharField(_("Type"), max_length=10, choices=TYPE_CHOICES) + amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) + notes = models.CharField(_("Notes"), max_length=255, blank=True) + + def __str__(self): + return f"{self.type.capitalize()}: {self.account.name_en} - {self.amount}" \ No newline at end of file diff --git a/accounting/signals.py b/accounting/signals.py new file mode 100644 index 0000000..20a792e --- /dev/null +++ b/accounting/signals.py @@ -0,0 +1,148 @@ +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() diff --git a/accounting/templates/accounting/account_ledger.html b/accounting/templates/accounting/account_ledger.html new file mode 100644 index 0000000..b23b1f4 --- /dev/null +++ b/accounting/templates/accounting/account_ledger.html @@ -0,0 +1,67 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+ +

+ {% trans "Ledger" %}: + {% if LANGUAGE_CODE == 'ar' %}{{ account.name_ar }}{% else %}{{ account.name_en }}{% endif %} + ({{ account.code }}) +

+
+
+

+ {% trans "Current Balance" %}: {{ total_balance|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }} +

+
+
+ +
+
+ + + + + + + + + + + + + {% for item_data in ledger_items %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Date" %}{% trans "Reference" %}{% trans "Description" %}{% trans "Debit" %}{% trans "Credit" %}{% trans "Balance" %}
{{ item_data.item.entry.date }}{{ item_data.item.entry.reference }}{{ item_data.item.entry.description }} + {% if item_data.item.type == 'debit' %}{{ item_data.item.amount|floatformat:global_settings.decimal_places }}{% endif %} + + {% if item_data.item.type == 'credit' %}{{ item_data.item.amount|floatformat:global_settings.decimal_places }}{% endif %} + + {{ item_data.balance|floatformat:global_settings.decimal_places }} +
{% trans "No transactions found for this account." %}
+
+
+
+{% endblock %} diff --git a/accounting/templates/accounting/balance_sheet.html b/accounting/templates/accounting/balance_sheet.html new file mode 100644 index 0000000..ad9598b --- /dev/null +++ b/accounting/templates/accounting/balance_sheet.html @@ -0,0 +1,123 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+ +

{% trans "Balance Sheet" %}

+

{% trans "As of" %} {{ date|date:"F d, Y" }}

+
+ +
+ +
+ +
+
+
+
{% trans "Assets" %}
+
+
+ + + {% for acc in assets %} + + + + + {% endfor %} + + + + + + + +
{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}{{ acc.balance|floatformat:global_settings.decimal_places }}
{% trans "Total Assets" %}{{ asset_total|floatformat:global_settings.decimal_places }}
+
+
+
+ + +
+
+
+
{% trans "Liabilities" %}
+
+
+ + + {% for acc in liabilities %} + + + + + {% endfor %} + + + + + + + +
{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}{{ acc.balance|floatformat:global_settings.decimal_places }}
{% trans "Total Liabilities" %}{{ liability_total|floatformat:global_settings.decimal_places }}
+
+
+ +
+
+
{% trans "Equity" %}
+
+
+ + + {% for acc in equity %} + + + + + {% endfor %} + + + + + + + + + + + +
{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}{{ acc.balance|floatformat:global_settings.decimal_places }}
{% trans "Net Income (Loss)" %}{{ net_income|floatformat:global_settings.decimal_places }}
{% trans "Total Equity" %}{{ equity_total|floatformat:global_settings.decimal_places }}
+
+
+ +
+
+
{% trans "Total Liabilities & Equity" %}
+
+ {{ liability_total|add:equity_total|floatformat:global_settings.decimal_places }} +
+
+
+
+
+
+ + +{% endblock %} diff --git a/accounting/templates/accounting/chart_of_accounts.html b/accounting/templates/accounting/chart_of_accounts.html new file mode 100644 index 0000000..e443a36 --- /dev/null +++ b/accounting/templates/accounting/chart_of_accounts.html @@ -0,0 +1,57 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+ +

{% trans "Chart of Accounts" %}

+
+
+ +
+
+ + + + + + + + + + + + {% for account in accounts %} + + + + + + + + {% endfor %} + +
{% trans "Code" %}{% trans "Account Name" %}{% trans "Type" %}{% trans "Balance" %}{% trans "Actions" %}
{{ account.code }} + {% if LANGUAGE_CODE == 'ar' %}{{ account.name_ar }}{% else %}{{ account.name_en }}{% endif %} + + + {{ account.get_account_type_display }} + + + {{ account.balance|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }} + + + {% trans "Ledger" %} + +
+
+
+
+{% endblock %} diff --git a/accounting/templates/accounting/dashboard.html b/accounting/templates/accounting/dashboard.html new file mode 100644 index 0000000..86225d3 --- /dev/null +++ b/accounting/templates/accounting/dashboard.html @@ -0,0 +1,107 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+

{% trans "Accounting Dashboard" %}

+
+ {% trans "Financial Summary" %} +
+
+ + +
+
+
+
+
{% trans "Total Assets" %}
+

{{ total_assets|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}

+
+
+
+
+
+
+
{% trans "Total Liabilities" %}
+

{{ total_liabilities|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}

+
+
+
+
+
+
+
{% trans "Monthly Revenue" %}
+

{{ monthly_revenue|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}

+
+
+
+
+
+
+
{% trans "Monthly Net Profit" %}
+

+ {{ net_profit|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }} +

+
+
+
+
+ + +
+ +
+ + +
+
+
{% trans "Recent Journal Entries" %}
+
+
+ + + + + + + + + + + {% for entry in recent_entries %} + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Date" %}{% trans "Reference" %}{% trans "Description" %}{% trans "Amount" %}
{{ entry.date }}{{ entry.reference }}{{ entry.description }} + {% with items=entry.items.all %} + {% for item in items %} + {% if item.type == 'debit' %} +
{{ item.amount|floatformat:global_settings.decimal_places }}
+ {% endif %} + {% endfor %} + {% endwith %} +
{% trans "No recent entries found." %}
+
+
+
+{% endblock %} diff --git a/accounting/templates/accounting/journal_entries.html b/accounting/templates/accounting/journal_entries.html new file mode 100644 index 0000000..f60b7ae --- /dev/null +++ b/accounting/templates/accounting/journal_entries.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+ +

{% trans "Journal Entries" %}

+
+
+ + {% for entry in entries %} +
+
+
+ {% trans "Date" %}: {{ entry.date }} + {% trans "Reference" %}: {{ entry.reference }} +
+
#{{ entry.id }}
+
+
+

{% trans "Description" %}: {{ entry.description }}

+
+ + + + + + + + + + {% for item in entry.items.all %} + + + + + + {% endfor %} + +
{% trans "Account" %}{% trans "Debit" %}{% trans "Credit" %}
+ {{ item.account.code }} - + {% if LANGUAGE_CODE == 'ar' %}{{ item.account.name_ar }}{% else %}{{ item.account.name_en }}{% endif %} + + {% if item.type == 'debit' %}{{ item.amount|floatformat:global_settings.decimal_places }}{% endif %} + + {% if item.type == 'credit' %}{{ item.amount|floatformat:global_settings.decimal_places }}{% endif %} +
+
+
+
+ {% empty %} +
+
+

{% trans "No journal entries found." %}

+
+
+ {% endfor %} +
+{% endblock %} diff --git a/accounting/templates/accounting/profit_loss.html b/accounting/templates/accounting/profit_loss.html new file mode 100644 index 0000000..d363d81 --- /dev/null +++ b/accounting/templates/accounting/profit_loss.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+ +

{% trans "Profit & Loss Statement" %}

+

{% trans "Period ending" %} {{ date|date:"F d, Y" }}

+
+ +
+ +
+
+
+
+ + + + + + + + + + + + + {% for acc in revenue_accounts %} + + + + + {% endfor %} + + + + + + + + + + {% for acc in expense_accounts %} + + + + + {% endfor %} + + + + + + + + + + + +
{% trans "Description" %}{% trans "Amount" %}
{% trans "REVENUE" %}
{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}{{ acc.balance|floatformat:global_settings.decimal_places }}
{% trans "Total Revenue" %}{{ revenue_total|floatformat:global_settings.decimal_places }}
{% trans "EXPENSES" %}
{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}({{ acc.balance|floatformat:global_settings.decimal_places }})
{% trans "Total Expenses" %}({{ expense_total|floatformat:global_settings.decimal_places }})
{% trans "NET PROFIT / LOSS" %}{{ net_profit|floatformat:global_settings.decimal_places }}
+
+
+
+
+
+ + +{% endblock %} diff --git a/accounting/templates/accounting/trial_balance.html b/accounting/templates/accounting/trial_balance.html new file mode 100644 index 0000000..314ebfe --- /dev/null +++ b/accounting/templates/accounting/trial_balance.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+ +

{% trans "Trial Balance" %}

+
+ +
+ +
+
+ + + + + + + + + + {% for data in trial_data %} + + + + + + {% endfor %} + + + + + + + + +
{% trans "Account" %}{% trans "Debit" %}{% trans "Credit" %}
+ {{ data.account.code }} - + {% if LANGUAGE_CODE == 'ar' %}{{ data.account.name_ar }}{% else %}{{ data.account.name_en }}{% endif %} + + {% if data.debit > 0 %}{{ data.debit|floatformat:global_settings.decimal_places }}{% endif %} + + {% if data.credit > 0 %}{{ data.credit|floatformat:global_settings.decimal_places }}{% endif %} +
{% trans "TOTAL" %}{{ total_debit|floatformat:global_settings.decimal_places }}{{ total_credit|floatformat:global_settings.decimal_places }}
+
+
+
+ + +{% endblock %} diff --git a/accounting/tests.py b/accounting/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounting/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounting/urls.py b/accounting/urls.py new file mode 100644 index 0000000..e9f86bb --- /dev/null +++ b/accounting/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.accounting_dashboard, name='accounting_dashboard'), + path('chart-of-accounts/', views.chart_of_accounts, name='chart_of_accounts'), + path('journal-entries/', views.journal_entries, name='journal_entries'), + path('ledger//', views.account_ledger, name='account_ledger'), + path('trial-balance/', views.trial_balance, name='trial_balance'), + path('balance-sheet/', views.balance_sheet, name='balance_sheet'), + path('profit-loss/', views.profit_loss, name='profit_loss'), +] diff --git a/accounting/views.py b/accounting/views.py new file mode 100644 index 0000000..4d80692 --- /dev/null +++ b/accounting/views.py @@ -0,0 +1,154 @@ +from django.shortcuts import render, get_object_or_404 +from django.contrib.auth.decorators import login_required +from .models import Account, JournalEntry, JournalItem +from django.db.models import Sum, Q +from django.utils import timezone +from datetime import datetime + +@login_required +def accounting_dashboard(request): + total_assets = sum(acc.balance for acc in Account.objects.filter(account_type='asset')) + total_liabilities = sum(acc.balance for acc in Account.objects.filter(account_type='liability')) + total_equity = sum(acc.balance for acc in Account.objects.filter(account_type='equity')) + + # Revenue and Expenses for current month + month_start = timezone.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0) + + revenue_items = JournalItem.objects.filter( + account__account_type='income', + entry__date__gte=month_start + ) + monthly_revenue = (revenue_items.filter(type='credit').aggregate(total=Sum('amount'))['total'] or 0) - \ + (revenue_items.filter(type='debit').aggregate(total=Sum('amount'))['total'] or 0) + + expense_items = JournalItem.objects.filter( + account__account_type='expense', + entry__date__gte=month_start + ) + monthly_expense = (expense_items.filter(type='debit').aggregate(total=Sum('amount'))['total'] or 0) - \ + (expense_items.filter(type='credit').aggregate(total=Sum('amount'))['total'] or 0) + + context = { + 'total_assets': total_assets, + 'total_liabilities': total_liabilities, + 'total_equity': total_equity, + 'monthly_revenue': monthly_revenue, + 'monthly_expense': monthly_expense, + 'net_profit': monthly_revenue - monthly_expense, + 'recent_entries': JournalEntry.objects.order_by('-date', '-id')[:10] + } + return render(request, 'accounting/dashboard.html', context) + +@login_required +def chart_of_accounts(request): + accounts = Account.objects.all().order_by('code') + return render(request, 'accounting/chart_of_accounts.html', {'accounts': accounts}) + +@login_required +def journal_entries(request): + entries = JournalEntry.objects.all().order_by('-date', '-id') + return render(request, 'accounting/journal_entries.html', {'entries': entries}) + +@login_required +def account_ledger(request, account_id): + account = get_object_or_404(Account, id=account_id) + items = JournalItem.objects.filter(account=account).order_by('entry__date', 'entry__id') + + # Calculate running balance + running_balance = 0 + ledger_items = [] + for item in items: + if account.account_type in ['asset', 'expense']: + change = item.amount if item.type == 'debit' else -item.amount + else: + change = item.amount if item.type == 'credit' else -item.amount + running_balance += change + ledger_items.append({ + 'item': item, + 'balance': running_balance + }) + + return render(request, 'accounting/account_ledger.html', { + 'account': account, + 'ledger_items': ledger_items, + 'total_balance': running_balance + }) + +@login_required +def trial_balance(request): + accounts = Account.objects.all().order_by('code') + trial_data = [] + total_debit = 0 + total_credit = 0 + + for acc in accounts: + items = acc.journal_items.all() + debits = items.filter(type='debit').aggregate(total=Sum('amount'))['total'] or 0 + credits = items.filter(type='credit').aggregate(total=Sum('amount'))['total'] or 0 + + if debits > 0 or credits > 0: + trial_data.append({ + 'account': acc, + 'debit': debits, + 'credit': credits + }) + total_debit += debits + total_credit += credits + + return render(request, 'accounting/trial_balance.html', { + 'trial_data': trial_data, + 'total_debit': total_debit, + 'total_credit': total_credit + }) + +@login_required +def balance_sheet(request): + assets = Account.objects.filter(account_type='asset') + liabilities = Account.objects.filter(account_type='liability') + equity = Account.objects.filter(account_type='equity') + + # Include Net Income in Equity + revenue = JournalItem.objects.filter(account__account_type='income').aggregate( + cr=Sum('amount', filter=Q(type='credit')), + dr=Sum('amount', filter=Q(type='debit')) + ) + net_revenue = (revenue['cr'] or 0) - (revenue['dr'] or 0) + + expenses = JournalItem.objects.filter(account__account_type='expense').aggregate( + dr=Sum('amount', filter=Q(type='debit')), + cr=Sum('amount', filter=Q(type='credit')) + ) + net_expenses = (expenses['dr'] or 0) - (expenses['cr'] or 0) + net_income = net_revenue - net_expenses + + asset_total = sum(acc.balance for acc in assets) + liability_total = sum(acc.balance for acc in liabilities) + equity_total = sum(acc.balance for acc in equity) + net_income + + return render(request, 'accounting/balance_sheet.html', { + 'assets': assets, + 'liabilities': liabilities, + 'equity': equity, + 'net_income': net_income, + 'asset_total': asset_total, + 'liability_total': liability_total, + 'equity_total': equity_total, + 'date': timezone.now() + }) + +@login_required +def profit_loss(request): + revenue_accounts = Account.objects.filter(account_type='income') + expense_accounts = Account.objects.filter(account_type='expense') + + revenue_total = sum(acc.balance for acc in revenue_accounts) + expense_total = sum(acc.balance for acc in expense_accounts) + + return render(request, 'accounting/profit_loss.html', { + 'revenue_accounts': revenue_accounts, + 'expense_accounts': expense_accounts, + 'revenue_total': revenue_total, + 'expense_total': expense_total, + 'net_profit': revenue_total - expense_total, + 'date': timezone.now() + }) \ No newline at end of file diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index ab578e6..f7be13a 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index fe03dcc..0a0635b 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 35aeb66..7f0b943 100644 --- a/config/settings.py +++ b/config/settings.py @@ -56,6 +56,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'core', + 'accounting', ] MIDDLEWARE = [ diff --git a/config/urls.py b/config/urls.py index 22b4d6a..b731c7d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -8,6 +8,7 @@ urlpatterns = [ path("accounts/", include("django.contrib.auth.urls")), path("i18n/", include("django.conf.urls.i18n")), path("", include("core.urls")), + path("accounting/", include("accounting.urls")), ] if settings.DEBUG: diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a1d345b..48736bb 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/migrations/0017_expensecategory_accounting_account.py b/core/migrations/0017_expensecategory_accounting_account.py new file mode 100644 index 0000000..baf360f --- /dev/null +++ b/core/migrations/0017_expensecategory_accounting_account.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.7 on 2026-02-03 03:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounting', '0001_initial'), + ('core', '0016_expensecategory_expense'), + ] + + operations = [ + migrations.AddField( + model_name='expensecategory', + name='accounting_account', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expense_categories', to='accounting.account'), + ), + ] diff --git a/core/migrations/__pycache__/0017_expensecategory_accounting_account.cpython-311.pyc b/core/migrations/__pycache__/0017_expensecategory_accounting_account.cpython-311.pyc new file mode 100644 index 0000000..3709cf9 Binary files /dev/null and b/core/migrations/__pycache__/0017_expensecategory_accounting_account.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 4c5b59e..157d084 100644 --- a/core/models.py +++ b/core/models.py @@ -105,6 +105,7 @@ class PaymentMethod(models.Model): return f"{self.name_en} / {self.name_ar}" class ExpenseCategory(models.Model): + accounting_account = models.ForeignKey('accounting.Account', on_delete=models.SET_NULL, null=True, blank=True, related_name='expense_categories') name_en = models.CharField(_("Name (English)"), max_length=100) name_ar = models.CharField(_("Name (Arabic)"), max_length=100) description = models.TextField(_("Description"), blank=True) diff --git a/core/templates/base.html b/core/templates/base.html index dd8e629..2668d34 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -179,6 +179,46 @@ {% if user.is_superuser or user.is_staff %} + + +