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 "Current Balance" %}: {{ total_balance|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}
+
+
+
+
+
+
+
+
+
+ | {% trans "Date" %} |
+ {% trans "Reference" %} |
+ {% trans "Description" %} |
+ {% trans "Debit" %} |
+ {% trans "Credit" %} |
+ {% trans "Balance" %} |
+
+
+
+ {% for item_data in ledger_items %}
+
+ | {{ 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 }}
+ |
+
+ {% empty %}
+
+ | {% trans "No transactions found for this account." %} |
+
+ {% endfor %}
+
+
+
+
+
+{% 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" }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for acc in assets %}
+
+ | {% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %} |
+ {{ acc.balance|floatformat:global_settings.decimal_places }} |
+
+ {% endfor %}
+
+
+
+ | {% trans "Total Assets" %} |
+ {{ asset_total|floatformat:global_settings.decimal_places }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for acc in liabilities %}
+
+ | {% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %} |
+ {{ acc.balance|floatformat:global_settings.decimal_places }} |
+
+ {% endfor %}
+
+
+
+ | {% trans "Total Liabilities" %} |
+ {{ liability_total|floatformat:global_settings.decimal_places }} |
+
+
+
+
+
+
+
+
+
+
+
+ {% for acc in equity %}
+
+ | {% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %} |
+ {{ acc.balance|floatformat:global_settings.decimal_places }} |
+
+ {% endfor %}
+
+ | {% 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 "Code" %} |
+ {% trans "Account Name" %} |
+ {% trans "Type" %} |
+ {% trans "Balance" %} |
+ {% trans "Actions" %} |
+
+
+
+ {% for account in accounts %}
+
+ | {{ 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" %}
+
+ |
+
+ {% endfor %}
+
+
+
+
+
+{% 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 "Date" %} |
+ {% trans "Reference" %} |
+ {% trans "Description" %} |
+ {% trans "Amount" %} |
+
+
+
+ {% for entry in recent_entries %}
+
+ | {{ 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 %}
+ |
+
+ {% empty %}
+
+ | {% trans "No recent entries found." %} |
+
+ {% endfor %}
+
+
+
+
+
+{% 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 %}
+
+
+
+ {% for entry in entries %}
+
+
+
+
{% trans "Description" %}: {{ entry.description }}
+
+
+
+
+ | {% trans "Account" %} |
+ {% trans "Debit" %} |
+ {% trans "Credit" %} |
+
+
+
+ {% for item in entry.items.all %}
+
+ |
+ {{ 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 %}
+ |
+
+ {% endfor %}
+
+
+
+
+
+ {% 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" }}
+
+
+
+
+
+
+
+
+
+
+
+ | {% trans "Description" %} |
+ {% trans "Amount" %} |
+
+
+
+
+
+ | {% trans "REVENUE" %} |
+
+ {% for acc in revenue_accounts %}
+
+ | {% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %} |
+ {{ acc.balance|floatformat:global_settings.decimal_places }} |
+
+ {% endfor %}
+
+ | {% trans "Total Revenue" %} |
+ {{ revenue_total|floatformat:global_settings.decimal_places }} |
+
+
+
+
+ | {% trans "EXPENSES" %} |
+
+ {% for acc in expense_accounts %}
+
+ | {% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %} |
+ ({{ acc.balance|floatformat:global_settings.decimal_places }}) |
+
+ {% endfor %}
+
+ | {% 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 "Account" %} |
+ {% trans "Debit" %} |
+ {% trans "Credit" %} |
+
+
+
+ {% for data in trial_data %}
+
+ |
+ {{ 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 %}
+ |
+
+ {% endfor %}
+
+
+
+ | {% 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 %}
+
+
+