adding accounting

This commit is contained in:
Flatlogic Bot 2026-02-03 03:17:21 +00:00
parent 0a02320029
commit bdafaca493
41 changed files with 1138 additions and 0 deletions

0
accounting/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
accounting/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

10
accounting/apps.py Normal file
View File

@ -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

View File

View File

@ -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}')

View File

@ -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')),
],
),
]

View File

73
accounting/models.py Normal file
View File

@ -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}"

148
accounting/signals.py Normal file
View File

@ -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()

View File

@ -0,0 +1,67 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'accounting_dashboard' %}">{% trans "Accounting" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'chart_of_accounts' %}">{% trans "Chart of Accounts" %}</a></li>
<li class="breadcrumb-item active">{% trans "Ledger" %}</li>
</ol>
</nav>
<h2 class="mb-0">
{% trans "Ledger" %}:
{% if LANGUAGE_CODE == 'ar' %}{{ account.name_ar }}{% else %}{{ account.name_en }}{% endif %}
<small class="text-muted">({{ account.code }})</small>
</h2>
</div>
<div class="text-end">
<h4 class="mb-0 text-primary">
{% trans "Current Balance" %}: {{ total_balance|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}
</h4>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Reference" %}</th>
<th>{% trans "Description" %}</th>
<th class="text-end">{% trans "Debit" %}</th>
<th class="text-end">{% trans "Credit" %}</th>
<th class="text-end">{% trans "Balance" %}</th>
</tr>
</thead>
<tbody>
{% for item_data in ledger_items %}
<tr>
<td>{{ item_data.item.entry.date }}</td>
<td><code>{{ item_data.item.entry.reference }}</code></td>
<td>{{ item_data.item.entry.description }}</td>
<td class="text-end text-success">
{% if item_data.item.type == 'debit' %}{{ item_data.item.amount|floatformat:global_settings.decimal_places }}{% endif %}
</td>
<td class="text-end text-danger">
{% if item_data.item.type == 'credit' %}{{ item_data.item.amount|floatformat:global_settings.decimal_places }}{% endif %}
</td>
<td class="text-end fw-bold">
{{ item_data.balance|floatformat:global_settings.decimal_places }}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-4 text-muted">{% trans "No transactions found for this account." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,123 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'accounting_dashboard' %}">{% trans "Accounting" %}</a></li>
<li class="breadcrumb-item active">{% trans "Balance Sheet" %}</li>
</ol>
</nav>
<h2 class="mb-0">{% trans "Balance Sheet" %}</h2>
<p class="text-muted small">{% trans "As of" %} {{ date|date:"F d, Y" }}</p>
</div>
<button class="btn btn-outline-primary" onclick="window.print()">
<i class="bi bi-printer"></i> {% trans "Print Report" %}
</button>
</div>
<div class="row g-4">
<!-- Assets -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">{% trans "Assets" %}</h5>
</div>
<div class="card-body p-0">
<table class="table mb-0">
<tbody>
{% for acc in assets %}
<tr>
<td>{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}</td>
<td class="text-end">{{ acc.balance|floatformat:global_settings.decimal_places }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="bg-light fw-bold">
<tr>
<td>{% trans "Total Assets" %}</td>
<td class="text-end">{{ asset_total|floatformat:global_settings.decimal_places }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- Liabilities & Equity -->
<div class="col-md-6">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">{% trans "Liabilities" %}</h5>
</div>
<div class="card-body p-0">
<table class="table mb-0">
<tbody>
{% for acc in liabilities %}
<tr>
<td>{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}</td>
<td class="text-end">{{ acc.balance|floatformat:global_settings.decimal_places }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="bg-light fw-bold">
<tr>
<td>{% trans "Total Liabilities" %}</td>
<td class="text-end">{{ liability_total|floatformat:global_settings.decimal_places }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">{% trans "Equity" %}</h5>
</div>
<div class="card-body p-0">
<table class="table mb-0">
<tbody>
{% for acc in equity %}
<tr>
<td>{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}</td>
<td class="text-end">{{ acc.balance|floatformat:global_settings.decimal_places }}</td>
</tr>
{% endfor %}
<tr>
<td>{% trans "Net Income (Loss)" %}</td>
<td class="text-end">{{ net_income|floatformat:global_settings.decimal_places }}</td>
</tr>
</tbody>
<tfoot class="bg-light fw-bold">
<tr>
<td>{% trans "Total Equity" %}</td>
<td class="text-end">{{ equity_total|floatformat:global_settings.decimal_places }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="card mt-4 border-primary border-2 shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center py-3 bg-light">
<h5 class="mb-0 fw-bold">{% trans "Total Liabilities & Equity" %}</h5>
<h5 class="mb-0 fw-bold text-primary">
{{ liability_total|add:equity_total|floatformat:global_settings.decimal_places }}
</h5>
</div>
</div>
</div>
</div>
</div>
<style>
@media print {
.breadcrumb, .btn, .sidebar, .header { display: none !important; }
.main-content { margin: 0 !important; padding: 0 !important; }
.card { break-inside: avoid; }
}
</style>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'accounting_dashboard' %}">{% trans "Accounting" %}</a></li>
<li class="breadcrumb-item active">{% trans "Chart of Accounts" %}</li>
</ol>
</nav>
<h2 class="mb-0">{% trans "Chart of Accounts" %}</h2>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th>{% trans "Code" %}</th>
<th>{% trans "Account Name" %}</th>
<th>{% trans "Type" %}</th>
<th class="text-end">{% trans "Balance" %}</th>
<th class="text-center">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for account in accounts %}
<tr>
<td><strong>{{ account.code }}</strong></td>
<td>
{% if LANGUAGE_CODE == 'ar' %}{{ account.name_ar }}{% else %}{{ account.name_en }}{% endif %}
</td>
<td>
<span class="badge {% if account.account_type == 'asset' %}bg-primary{% elif account.account_type == 'liability' %}bg-danger{% elif account.account_type == 'income' %}bg-success{% elif account.account_type == 'expense' %}bg-warning text-dark{% else %}bg-secondary{% endif %}">
{{ account.get_account_type_display }}
</span>
</td>
<td class="text-end">
{{ account.balance|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}
</td>
<td class="text-center">
<a href="{% url 'account_ledger' account.id %}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-list-columns"></i> {% trans "Ledger" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,107 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">{% trans "Accounting Dashboard" %}</h2>
<div>
<span class="badge bg-primary">{% trans "Financial Summary" %}</span>
</div>
</div>
<!-- Summary Cards -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h6 class="text-muted mb-2">{% trans "Total Assets" %}</h6>
<h3 class="mb-0 text-primary">{{ total_assets|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h6 class="text-muted mb-2">{% trans "Total Liabilities" %}</h6>
<h3 class="mb-0 text-danger">{{ total_liabilities|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h6 class="text-muted mb-2">{% trans "Monthly Revenue" %}</h6>
<h3 class="mb-0 text-success">{{ monthly_revenue|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h6 class="text-muted mb-2">{% trans "Monthly Net Profit" %}</h6>
<h3 class="mb-0 {% if net_profit >= 0 %}text-success{% else %}text-danger{% endif %}">
{{ net_profit|floatformat:global_settings.decimal_places }} {{ global_settings.currency_symbol }}
</h3>
</div>
</div>
</div>
</div>
<!-- Quick Links -->
<div class="row g-3 mb-4">
<div class="col-md-12">
<div class="card border-0 shadow-sm">
<div class="card-body d-flex gap-2">
<a href="{% url 'chart_of_accounts' %}" class="btn btn-outline-primary">{% trans "Chart of Accounts" %}</a>
<a href="{% url 'journal_entries' %}" class="btn btn-outline-primary">{% trans "Journal Entries" %}</a>
<a href="{% url 'trial_balance' %}" class="btn btn-outline-info">{% trans "Trial Balance" %}</a>
<a href="{% url 'balance_sheet' %}" class="btn btn-outline-info">{% trans "Balance Sheet" %}</a>
<a href="{% url 'profit_loss' %}" class="btn btn-outline-info">{% trans "Profit & Loss" %}</a>
</div>
</div>
</div>
</div>
<!-- Recent Journal Entries -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">{% trans "Recent Journal Entries" %}</h5>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Reference" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Amount" %}</th>
</tr>
</thead>
<tbody>
{% for entry in recent_entries %}
<tr>
<td>{{ entry.date }}</td>
<td><code>{{ entry.reference }}</code></td>
<td>{{ entry.description }}</td>
<td>
{% with items=entry.items.all %}
{% for item in items %}
{% if item.type == 'debit' %}
<div class="text-nowrap">{{ item.amount|floatformat:global_settings.decimal_places }}</div>
{% endif %}
{% endfor %}
{% endwith %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">{% trans "No recent entries found." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,66 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'accounting_dashboard' %}">{% trans "Accounting" %}</a></li>
<li class="breadcrumb-item active">{% trans "Journal Entries" %}</li>
</ol>
</nav>
<h2 class="mb-0">{% trans "Journal Entries" %}</h2>
</div>
</div>
{% for entry in entries %}
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<div>
<span class="text-muted small">{% trans "Date" %}:</span> <strong>{{ entry.date }}</strong>
<span class="ms-3 text-muted small">{% trans "Reference" %}:</span> <code>{{ entry.reference }}</code>
</div>
<div class="text-muted small">#{{ entry.id }}</div>
</div>
<div class="card-body">
<p class="mb-3"><strong>{% trans "Description" %}:</strong> {{ entry.description }}</p>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0">
<thead class="bg-light">
<tr>
<th>{% trans "Account" %}</th>
<th class="text-end" style="width: 150px;">{% trans "Debit" %}</th>
<th class="text-end" style="width: 150px;">{% trans "Credit" %}</th>
</tr>
</thead>
<tbody>
{% for item in entry.items.all %}
<tr>
<td>
{{ item.account.code }} -
{% if LANGUAGE_CODE == 'ar' %}{{ item.account.name_ar }}{% else %}{{ item.account.name_en }}{% endif %}
</td>
<td class="text-end">
{% if item.type == 'debit' %}{{ item.amount|floatformat:global_settings.decimal_places }}{% endif %}
</td>
<td class="text-end">
{% if item.type == 'credit' %}{{ item.amount|floatformat:global_settings.decimal_places }}{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% empty %}
<div class="card border-0 shadow-sm">
<div class="card-body text-center py-5">
<p class="text-muted mb-0">{% trans "No journal entries found." %}</p>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'accounting_dashboard' %}">{% trans "Accounting" %}</a></li>
<li class="breadcrumb-item active">{% trans "Profit & Loss" %}</li>
</ol>
</nav>
<h2 class="mb-0">{% trans "Profit & Loss Statement" %}</h2>
<p class="text-muted small">{% trans "Period ending" %} {{ date|date:"F d, Y" }}</p>
</div>
<button class="btn btn-outline-primary" onclick="window.print()">
<i class="bi bi-printer"></i> {% trans "Print Report" %}
</button>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<table class="table mb-0">
<thead class="bg-primary text-white">
<tr>
<th class="py-3 ps-4">{% trans "Description" %}</th>
<th class="text-end py-3 pe-4">{% trans "Amount" %}</th>
</tr>
</thead>
<tbody>
<!-- Revenue -->
<tr class="bg-light fw-bold">
<td colspan="2" class="ps-4">{% trans "REVENUE" %}</td>
</tr>
{% for acc in revenue_accounts %}
<tr>
<td class="ps-5">{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}</td>
<td class="text-end pe-4">{{ acc.balance|floatformat:global_settings.decimal_places }}</td>
</tr>
{% endfor %}
<tr class="fw-bold">
<td class="ps-4">{% trans "Total Revenue" %}</td>
<td class="text-end pe-4 border-top">{{ revenue_total|floatformat:global_settings.decimal_places }}</td>
</tr>
<!-- Expenses -->
<tr class="bg-light fw-bold">
<td colspan="2" class="ps-4 mt-4">{% trans "EXPENSES" %}</td>
</tr>
{% for acc in expense_accounts %}
<tr>
<td class="ps-5">{% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}</td>
<td class="text-end pe-4">({{ acc.balance|floatformat:global_settings.decimal_places }})</td>
</tr>
{% endfor %}
<tr class="fw-bold">
<td class="ps-4">{% trans "Total Expenses" %}</td>
<td class="text-end pe-4 border-top">({{ expense_total|floatformat:global_settings.decimal_places }})</td>
</tr>
</tbody>
<tfoot class="bg-dark text-white fw-bold">
<tr>
<td class="py-3 ps-4">{% trans "NET PROFIT / LOSS" %}</td>
<td class="text-end py-3 pe-4">{{ net_profit|floatformat:global_settings.decimal_places }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
<style>
@media print {
.breadcrumb, .btn, .sidebar, .header { display: none !important; }
.main-content { margin: 0 !important; padding: 0 !important; }
}
</style>
{% endblock %}

View File

@ -0,0 +1,66 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'accounting_dashboard' %}">{% trans "Accounting" %}</a></li>
<li class="breadcrumb-item active">{% trans "Trial Balance" %}</li>
</ol>
</nav>
<h2 class="mb-0">{% trans "Trial Balance" %}</h2>
</div>
<button class="btn btn-outline-primary" onclick="window.print()">
<i class="bi bi-printer"></i> {% trans "Print Report" %}
</button>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-bordered align-middle mb-0">
<thead class="bg-light">
<tr>
<th>{% trans "Account" %}</th>
<th class="text-end" style="width: 200px;">{% trans "Debit" %}</th>
<th class="text-end" style="width: 200px;">{% trans "Credit" %}</th>
</tr>
</thead>
<tbody>
{% for data in trial_data %}
<tr>
<td>
<strong>{{ data.account.code }}</strong> -
{% if LANGUAGE_CODE == 'ar' %}{{ data.account.name_ar }}{% else %}{{ data.account.name_en }}{% endif %}
</td>
<td class="text-end">
{% if data.debit > 0 %}{{ data.debit|floatformat:global_settings.decimal_places }}{% endif %}
</td>
<td class="text-end">
{% if data.credit > 0 %}{{ data.credit|floatformat:global_settings.decimal_places }}{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="bg-light fw-bold">
<tr>
<td class="text-end">{% trans "TOTAL" %}</td>
<td class="text-end border-top border-dark border-3">{{ total_debit|floatformat:global_settings.decimal_places }}</td>
<td class="text-end border-top border-dark border-3">{{ total_credit|floatformat:global_settings.decimal_places }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<style>
@media print {
.breadcrumb, .btn, .sidebar, .header { display: none !important; }
.main-content { margin: 0 !important; padding: 0 !important; }
.card { border: none !important; shadow: none !important; }
}
</style>
{% endblock %}

3
accounting/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
accounting/urls.py Normal file
View File

@ -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/<int:account_id>/', 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'),
]

154
accounting/views.py Normal file
View File

@ -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()
})

View File

@ -56,6 +56,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
'accounting',
]
MIDDLEWARE = [

View File

@ -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:

View File

@ -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'),
),
]

View File

@ -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)

View File

@ -179,6 +179,46 @@
{% if user.is_superuser or user.is_staff %}
<!-- Accounting Group -->
<li class="sidebar-group-header mt-2">
<a href="#accountingSubmenu" data-bs-toggle="collapse" aria-expanded="{% if 'accounting' in path %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
<span>{% trans "Accounting" %}</span>
<i class="bi bi-chevron-down chevron"></i>
</a>
<ul class="collapse list-unstyled sub-menu {% if 'accounting' in path %}show{% endif %}" id="accountingSubmenu">
<li>
<a href="{% url 'accounting_dashboard' %}" class="{% if url_name == 'accounting_dashboard' %}active{% endif %}">
<i class="bi bi-calculator"></i> {% trans "Dashboard" %}
</a>
</li>
<li>
<a href="{% url 'chart_of_accounts' %}" class="{% if url_name == 'chart_of_accounts' %}active{% endif %}">
<i class="bi bi-list-task"></i> {% trans "Chart of Accounts" %}
</a>
</li>
<li>
<a href="{% url 'journal_entries' %}" class="{% if url_name == 'journal_entries' %}active{% endif %}">
<i class="bi bi-journal-text"></i> {% trans "Journal Entries" %}
</a>
</li>
<li>
<a href="{% url 'trial_balance' %}" class="{% if url_name == 'trial_balance' %}active{% endif %}">
<i class="bi bi-check2-square"></i> {% trans "Trial Balance" %}
<li>
<a href="{% url 'balance_sheet' %}" class="{% if url_name == 'balance_sheet' %}active{% endif %}">
<i class="bi bi-journal-check"></i> {% trans "Balance Sheet" %}
</a>
</li>
<li>
<a href="{% url 'profit_loss' %}" class="{% if url_name == 'profit_loss' %}active{% endif %}">
<i class="bi bi-bar-chart"></i> {% trans "Profit & Loss" %}
</a>
</li>
</a>
</li>
</ul>
</li>
<!-- Reports Group -->
<li class="sidebar-group-header mt-2">
<a href="#reportsSubmenu" data-bs-toggle="collapse" aria-expanded="{% if url_name == 'reports' or url_name == 'customer_statement' or url_name == 'supplier_statement' or url_name == 'cashflow_report' %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">