adding accounting
This commit is contained in:
parent
0a02320029
commit
bdafaca493
0
accounting/__init__.py
Normal file
0
accounting/__init__.py
Normal file
BIN
accounting/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
accounting/__pycache__/admin.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
accounting/__pycache__/apps.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
accounting/__pycache__/models.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
accounting/__pycache__/signals.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/signals.cpython-311.pyc
Normal file
Binary file not shown.
BIN
accounting/__pycache__/urls.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
accounting/__pycache__/views.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
3
accounting/admin.py
Normal file
3
accounting/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
10
accounting/apps.py
Normal file
10
accounting/apps.py
Normal 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
|
||||
0
accounting/management/__init__.py
Normal file
0
accounting/management/__init__.py
Normal file
BIN
accounting/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
accounting/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
0
accounting/management/commands/__init__.py
Normal file
0
accounting/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
47
accounting/management/commands/setup_accounts.py
Normal file
47
accounting/management/commands/setup_accounts.py
Normal 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}')
|
||||
56
accounting/migrations/0001_initial.py
Normal file
56
accounting/migrations/0001_initial.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
accounting/migrations/__init__.py
Normal file
0
accounting/migrations/__init__.py
Normal file
BIN
accounting/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
accounting/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
BIN
accounting/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
accounting/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
73
accounting/models.py
Normal file
73
accounting/models.py
Normal 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
148
accounting/signals.py
Normal 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()
|
||||
67
accounting/templates/accounting/account_ledger.html
Normal file
67
accounting/templates/accounting/account_ledger.html
Normal 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 %}
|
||||
123
accounting/templates/accounting/balance_sheet.html
Normal file
123
accounting/templates/accounting/balance_sheet.html
Normal 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 %}
|
||||
57
accounting/templates/accounting/chart_of_accounts.html
Normal file
57
accounting/templates/accounting/chart_of_accounts.html
Normal 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 %}
|
||||
107
accounting/templates/accounting/dashboard.html
Normal file
107
accounting/templates/accounting/dashboard.html
Normal 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 %}
|
||||
66
accounting/templates/accounting/journal_entries.html
Normal file
66
accounting/templates/accounting/journal_entries.html
Normal 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 %}
|
||||
83
accounting/templates/accounting/profit_loss.html
Normal file
83
accounting/templates/accounting/profit_loss.html
Normal 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 %}
|
||||
66
accounting/templates/accounting/trial_balance.html
Normal file
66
accounting/templates/accounting/trial_balance.html
Normal 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
3
accounting/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
12
accounting/urls.py
Normal file
12
accounting/urls.py
Normal 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
154
accounting/views.py
Normal 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()
|
||||
})
|
||||
Binary file not shown.
Binary file not shown.
@ -56,6 +56,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'core',
|
||||
'accounting',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@ -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:
|
||||
|
||||
Binary file not shown.
20
core/migrations/0017_expensecategory_accounting_account.py
Normal file
20
core/migrations/0017_expensecategory_accounting_account.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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)
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user