Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7166fdc776 | ||
|
|
d28c59d57b | ||
|
|
8f6e105aac | ||
|
|
c8c0620ceb | ||
|
|
fa0a735548 | ||
|
|
03fe74ce32 | ||
|
|
2a2b761270 | ||
|
|
e405332220 | ||
|
|
7f2a24bae9 | ||
|
|
d973c05c15 | ||
|
|
bf533af9e8 | ||
|
|
a30bc16ccc | ||
|
|
48923270af | ||
|
|
a9b274a48f | ||
|
|
0649b20627 | ||
|
|
739a0d397f | ||
|
|
cfa7d80ecc | ||
|
|
1fe85ff3ce | ||
|
|
b30330b17b | ||
|
|
55c69b5fdc | ||
|
|
b66ef8649e | ||
|
|
46a59fc51c | ||
|
|
e6865dae2e | ||
|
|
835d5ab1de | ||
|
|
7c0bb7cb38 | ||
|
|
b2602b999f | ||
|
|
441b04ab78 | ||
|
|
c851649b1a | ||
|
|
d339a98349 | ||
|
|
80c69b02d2 | ||
|
|
843c1807e7 | ||
|
|
9e26d44a3c | ||
|
|
2d9bb3f05b | ||
|
|
8f414c4f6d | ||
|
|
9299fde7e7 | ||
|
|
db1a6f5278 | ||
|
|
45bc0c273e | ||
|
|
4c2a5f7938 | ||
|
|
f4761157f9 | ||
|
|
89eb33ae77 | ||
|
|
ee5b4ff280 | ||
|
|
8150c2ba43 | ||
|
|
d0b49e0c8a | ||
|
|
c79ace1553 | ||
|
|
5021756176 | ||
|
|
37d8922069 | ||
|
|
6298063c20 | ||
|
|
6b4a3fe6e7 | ||
|
|
a59b0c9341 | ||
|
|
9d3b9739fa | ||
|
|
42e2393347 | ||
|
|
40404a2947 | ||
|
|
bdde0c2da9 | ||
|
|
54a353ee6a | ||
|
|
73951729f9 | ||
|
|
49c4d4dab1 | ||
|
|
77e86e4c7b | ||
|
|
75d9aea042 | ||
|
|
c2c1cb5d82 | ||
|
|
850f4f6187 | ||
|
|
55438579ee | ||
|
|
2ad0af108e | ||
|
|
9dfa03d69c | ||
|
|
a123d9bb27 | ||
|
|
5c5625595e | ||
|
|
3f9709efef | ||
|
|
e473add476 | ||
|
|
00d9114ba0 | ||
|
|
f19ade40ee | ||
|
|
573f45e183 | ||
|
|
b0192498f4 | ||
|
|
bdafaca493 | ||
|
|
0a02320029 | ||
|
|
7a5e1a7044 | ||
|
|
82334fa523 | ||
|
|
34d6321e11 | ||
|
|
473f13fb08 | ||
|
|
0ae32328a7 | ||
|
|
e9c5a5c213 | ||
|
|
ddd4aa0397 | ||
|
|
a2c308c26c | ||
|
|
1e0d4f6540 | ||
|
|
f80934e391 | ||
|
|
5391ba1010 | ||
|
|
7d1c8df2b2 |
9
.gitignore
vendored
@ -1,3 +1,12 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*/node_modules/
|
*/node_modules/
|
||||||
*/build/
|
*/build/
|
||||||
|
.env
|
||||||
|
db.sqlite3
|
||||||
|
media/
|
||||||
|
staticfiles/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
|||||||
33
Dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Use official Python runtime as a parent image
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
default-libmysqlclient-dev \
|
||||||
|
build-essential \
|
||||||
|
pkg-config \
|
||||||
|
netcat-openbsd \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set work directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
RUN pip install --upgrade pip && pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy project
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# Make entrypoint executable
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Define entrypoint
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
0
accounting/__init__.py
Normal file
BIN
accounting/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/admin.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/apps.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/forms.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/models.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/signals.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/urls.cpython-311.pyc
Normal file
BIN
accounting/__pycache__/views.cpython-311.pyc
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
@ -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
|
||||||
26
accounting/forms.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import Account, JournalEntry, JournalItem
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
class AccountForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Account
|
||||||
|
fields = ['code', 'name_en', 'name_ar', 'account_type', 'description', 'is_active']
|
||||||
|
widgets = {
|
||||||
|
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'name_en': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'name_ar': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'account_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
class JournalEntryForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = JournalEntry
|
||||||
|
fields = ['date', 'description', 'reference']
|
||||||
|
widgets = {
|
||||||
|
'date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||||
|
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
||||||
|
'reference': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
}
|
||||||
0
accounting/management/__init__.py
Normal file
BIN
accounting/management/__pycache__/__init__.cpython-311.pyc
Normal file
0
accounting/management/commands/__init__.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
@ -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
BIN
accounting/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
accounting/migrations/__pycache__/__init__.cpython-311.pyc
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}"
|
||||||
167
accounting/signals.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models.signals import post_save, post_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from core.models import Sale, SalePayment, Purchase, PurchasePayment, Expense, SaleReturn, PurchaseReturn
|
||||||
|
from .models import Account, JournalEntry, JournalItem
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
def get_account(code):
|
||||||
|
try:
|
||||||
|
return Account.objects.get(code=code)
|
||||||
|
except Account.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_journal_entry(obj, description, items):
|
||||||
|
"""
|
||||||
|
items: list of dicts {'account': Account, 'type': 'debit'/'credit', 'amount': Decimal}
|
||||||
|
"""
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
|
||||||
|
# Delete existing entry if any (to handle updates)
|
||||||
|
JournalEntry.objects.filter(content_type=content_type, object_id=obj.id).delete()
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Filter out items with 0 amount
|
||||||
|
valid_items = []
|
||||||
|
for item in items:
|
||||||
|
try:
|
||||||
|
# Ensure amount is Decimal
|
||||||
|
amount = Decimal(str(item['amount']))
|
||||||
|
if amount > 0:
|
||||||
|
item['amount'] = amount
|
||||||
|
valid_items.append(item)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not valid_items:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Determine Entry Date
|
||||||
|
entry_date = timezone.now().date()
|
||||||
|
if hasattr(obj, 'date'):
|
||||||
|
entry_date = obj.date
|
||||||
|
elif hasattr(obj, 'payment_date'):
|
||||||
|
entry_date = obj.payment_date
|
||||||
|
elif hasattr(obj, 'created_at'):
|
||||||
|
entry_date = obj.created_at.date()
|
||||||
|
|
||||||
|
entry = JournalEntry.objects.create(
|
||||||
|
date=entry_date,
|
||||||
|
description=description,
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=obj.id,
|
||||||
|
reference=f"{obj.__class__.__name__} #{obj.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in valid_items:
|
||||||
|
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')
|
||||||
|
|
||||||
|
if not ar_acc or not sales_acc:
|
||||||
|
return
|
||||||
|
|
||||||
|
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_form.html
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<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">{% if account %}{% trans "Edit Account" %}{% else %}{% trans "Add Account" %}{% endif %}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h2 class="mb-0">{% if account %}{% trans "Edit Account" %}{% else %}{% trans "Add Account" %}{% endif %}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{% trans "Account Code" %}</label>
|
||||||
|
{{ form.code }}
|
||||||
|
{% if form.code.errors %}<div class="text-danger small">{{ form.code.errors }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">{% trans "Account Type" %}</label>
|
||||||
|
{{ form.account_type }}
|
||||||
|
{% if form.account_type.errors %}<div class="text-danger small">{{ form.account_type.errors }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% trans "Name (English)" %}</label>
|
||||||
|
{{ form.name_en }}
|
||||||
|
{% if form.name_en.errors %}<div class="text-danger small">{{ form.name_en.errors }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% trans "Name (Arabic)" %}</label>
|
||||||
|
{{ form.name_ar }}
|
||||||
|
{% if form.name_ar.errors %}<div class="text-danger small">{{ form.name_ar.errors }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">{% trans "Description" %}</label>
|
||||||
|
{{ form.description }}
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.is_active }}
|
||||||
|
<label class="form-check-label">{% trans "Is Active" %}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 pt-3 border-top d-flex justify-content-end gap-2">
|
||||||
|
<a href="{% url 'chart_of_accounts' %}" class="btn btn-light">{% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" class="btn btn-primary px-4">{% trans "Save Account" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
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
@ -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 %}
|
||||||
65
accounting/templates/accounting/chart_of_accounts.html
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{% 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>
|
||||||
|
<a href="{% url 'account_create' %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-lg"></i> {% trans "Add Account" %}
|
||||||
|
</a>
|
||||||
|
</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">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{% url 'account_ledger' account.id %}" class="btn btn-sm btn-outline-primary" title="{% trans 'Ledger' %}">
|
||||||
|
<i class="bi bi-list-columns"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'account_edit' account.id %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
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 %}
|
||||||
104
accounting/templates/accounting/journal_entries.html
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{% 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>
|
||||||
|
<a href="{% url 'manual_journal_entry' %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-lg"></i> {% trans "New Manual Entry" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="bg-light">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4">{% trans "Date" %}</th>
|
||||||
|
<th>{% trans "Ref #" %}</th>
|
||||||
|
<th>{% trans "Description" %}</th>
|
||||||
|
<th class="text-end">{% trans "Debit" %}</th>
|
||||||
|
<th class="text-end">{% trans "Credit" %}</th>
|
||||||
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in entries %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 text-nowrap">{{ entry.date }}</td>
|
||||||
|
<td>
|
||||||
|
{% if entry.reference %}
|
||||||
|
<span class="badge bg-light text-dark border">{{ entry.reference }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ entry.description|truncatechars:60 }}</td>
|
||||||
|
<td class="text-end fw-bold text-success">{{ entry.total_debit|floatformat:global_settings.decimal_places }}</td>
|
||||||
|
<td class="text-end fw-bold text-danger">{{ entry.total_credit|floatformat:global_settings.decimal_places }}</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#entry-details-{{ entry.id }}" aria-expanded="false">
|
||||||
|
<i class="bi bi-eye"></i> {% trans "View" %}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Details Row -->
|
||||||
|
<tr class="collapse bg-light" id="entry-details-{{ entry.id }}">
|
||||||
|
<td colspan="6" class="p-0">
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h6 class="mb-0 text-muted small text-uppercase fw-bold">{% trans "Transaction Details" %} #{{ entry.id }}</h6>
|
||||||
|
<small class="text-muted">{% trans "Created" %}: {{ entry.created_at|date:"Y-m-d H:i" }}</small>
|
||||||
|
</div>
|
||||||
|
<table class="table table-sm table-bordered bg-white mb-0">
|
||||||
|
<thead>
|
||||||
|
<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>
|
||||||
|
<span class="fw-medium">{{ item.account.code }}</span> -
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}{{ item.account.name_ar }}{% else %}{{ item.account.name_en }}{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end text-success">
|
||||||
|
{% if item.type == 'debit' %}{{ item.amount|floatformat:global_settings.decimal_places }}{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end text-danger">
|
||||||
|
{% if item.type == 'credit' %}{{ item.amount|floatformat:global_settings.decimal_places }}{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center py-5 text-muted">
|
||||||
|
{% trans "No journal entries found." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
202
accounting/templates/accounting/journal_entry_form.html
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-10">
|
||||||
|
<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 'journal_entries' %}">{% trans "Journal Entries" %}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{% trans "New Manual Entry" %}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h2 class="mb-0">{% trans "New Manual Journal Entry" %}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" id="journal-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">{% trans "Date" %}</label>
|
||||||
|
{{ form.date }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">{% trans "Reference" %}</label>
|
||||||
|
{{ form.reference }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% trans "Description" %}</label>
|
||||||
|
{{ form.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0" id="items-table">
|
||||||
|
<thead class="bg-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40%;">{% trans "Account" %}</th>
|
||||||
|
<th style="width: 20%;">{% trans "Type" %}</th>
|
||||||
|
<th style="width: 30%;">{% trans "Amount" %}</th>
|
||||||
|
<th style="width: 10%;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="item-row">
|
||||||
|
<td>
|
||||||
|
<select name="account[]" class="form-select select2-account" required>
|
||||||
|
<option value="">{% trans "Select Account" %}</option>
|
||||||
|
{% for acc in accounts %}
|
||||||
|
<option value="{{ acc.id }}">{{ acc.code }} - {% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="type[]" class="form-select item-type" required>
|
||||||
|
<option value="debit">{% trans "Debit" %}</option>
|
||||||
|
<option value="credit">{% trans "Credit" %}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" name="amount[]" class="form-control item-amount" step="0.001" min="0" required>
|
||||||
|
<span class="input-group-text">{{ global_settings.currency_symbol }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="bi bi-trash"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="item-row">
|
||||||
|
<td>
|
||||||
|
<select name="account[]" class="form-select select2-account" required>
|
||||||
|
<option value="">{% trans "Select Account" %}</option>
|
||||||
|
{% for acc in accounts %}
|
||||||
|
<option value="{{ acc.id }}">{{ acc.code }} - {% if LANGUAGE_CODE == 'ar' %}{{ acc.name_ar }}{% else %}{{ acc.name_en }}{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="type[]" class="form-select item-type" required>
|
||||||
|
<option value="debit">{% trans "Debit" %}</option>
|
||||||
|
<option value="credit" selected>{% trans "Credit" %}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" name="amount[]" class="form-control item-amount" step="0.001" min="0" required>
|
||||||
|
<span class="input-group-text">{{ global_settings.currency_symbol }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="bi bi-trash"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="bg-light">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm" id="add-row">
|
||||||
|
<i class="bi bi-plus-lg"></i> {% trans "Add Line" %}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="fw-bold">
|
||||||
|
<td class="text-end">{% trans "Totals" %}:</td>
|
||||||
|
<td class="text-end">{% trans "Debit" %}: <span id="total-debit">0.000</span></td>
|
||||||
|
<td class="text-end">{% trans "Credit" %}: <span id="total-credit">0.000</span></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-2" id="balance-message">
|
||||||
|
<span class="badge bg-danger">{% trans "Out of Balance" %}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-white p-4 border-top d-flex justify-content-end gap-2">
|
||||||
|
<a href="{% url 'journal_entries' %}" class="btn btn-light">{% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" class="btn btn-primary px-4" id="submit-btn" disabled>{% trans "Create Entry" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const table = document.getElementById('items-table').getElementsByTagName('tbody')[0];
|
||||||
|
const addBtn = document.getElementById('add-row');
|
||||||
|
const totalDebitSpan = document.getElementById('total-debit');
|
||||||
|
const totalCreditSpan = document.getElementById('total-credit');
|
||||||
|
const balanceMessage = document.getElementById('balance-message');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
|
||||||
|
function updateTotals() {
|
||||||
|
let totalDebit = 0;
|
||||||
|
let totalCredit = 0;
|
||||||
|
|
||||||
|
document.querySelectorAll('.item-row').forEach(row => {
|
||||||
|
const type = row.querySelector('.item-type').value;
|
||||||
|
const amount = parseFloat(row.querySelector('.item-amount').value) || 0;
|
||||||
|
|
||||||
|
if (type === 'debit') totalDebit += amount;
|
||||||
|
else totalCredit += amount;
|
||||||
|
});
|
||||||
|
|
||||||
|
totalDebitSpan.textContent = totalDebit.toFixed(3);
|
||||||
|
totalCreditSpan.textContent = totalCredit.toFixed(3);
|
||||||
|
|
||||||
|
const balanced = totalDebit > 0 && Math.abs(totalDebit - totalCredit) < 0.001;
|
||||||
|
|
||||||
|
if (balanced) {
|
||||||
|
balanceMessage.innerHTML = '<span class="badge bg-success">{% trans "Balanced" %}</span>';
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
} else {
|
||||||
|
balanceMessage.innerHTML = '<span class="badge bg-danger">{% trans "Out of Balance" %}</span>';
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addBtn.addEventListener('click', function() {
|
||||||
|
const firstRow = document.querySelector('.item-row');
|
||||||
|
const newRow = firstRow.cloneNode(true);
|
||||||
|
newRow.querySelector('.item-amount').value = '';
|
||||||
|
table.appendChild(newRow);
|
||||||
|
|
||||||
|
newRow.querySelector('.remove-row').addEventListener('click', function() {
|
||||||
|
if (document.querySelectorAll('.item-row').length > 2) {
|
||||||
|
newRow.remove();
|
||||||
|
updateTotals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newRow.querySelector('.item-type').addEventListener('change', updateTotals);
|
||||||
|
newRow.querySelector('.item-amount').addEventListener('input', updateTotals);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.remove-row').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
if (document.querySelectorAll('.item-row').length > 2) {
|
||||||
|
btn.closest('.item-row').remove();
|
||||||
|
updateTotals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.item-type').forEach(el => el.addEventListener('change', updateTotals));
|
||||||
|
document.querySelectorAll('.item-amount').forEach(el => el.addEventListener('input', updateTotals));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
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
@ -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 %}
|
||||||
121
accounting/templates/accounting/vat_report.html
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-0">{% trans "VAT Report" %} / تقرير ضريبة القيمة المضافة</h2>
|
||||||
|
<p class="text-muted">{% trans "Tax Declaration Summary" %} / ملخص الإقرار الضريبي</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onclick="window.print()" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-printer"></i> {% trans "Print" %} / طباعة
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Form -->
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{% trans "Start Date" %} / تاريخ البدء</label>
|
||||||
|
<input type="date" name="start_date" class="form-control" value="{{ start_date }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{% trans "End Date" %} / تاريخ الانتهاء</label>
|
||||||
|
<input type="date" name="end_date" class="form-control" value="{{ end_date }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="bi bi-filter"></i> {% trans "Generate Report" %} / إنشاء التقرير
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Report Content -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Sales / Output Tax -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-header bg-light py-3">
|
||||||
|
<h5 class="card-title mb-0 text-primary">
|
||||||
|
1. {% trans "Sales (Output VAT)" %} / المبيعات (ضريبة المخرجات)
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-striped mb-0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="py-3">{% trans "Total Sales (Excl. VAT)" %} <br> <small class="text-muted">إجمالي المبيعات (غير شامل الضريبة)</small></td>
|
||||||
|
<td class="text-end py-3 fw-bold">{{ total_sales_subtotal|floatformat:3 }} {{ global_settings.currency_symbol }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-3">{% trans "Total VAT Collected" %} <br> <small class="text-muted">إجمالي الضريبة المحصلة</small></td>
|
||||||
|
<td class="text-end py-3 fw-bold text-danger">{{ total_output_vat|floatformat:3 }} {{ global_settings.currency_symbol }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-primary">
|
||||||
|
<td class="py-3">{% trans "Total Gross Sales" %} <br> <small class="text-muted">إجمالي المبيعات (شامل الضريبة)</small></td>
|
||||||
|
<td class="text-end py-3 fw-bold">{{ total_sales_gross|floatformat:3 }} {{ global_settings.currency_symbol }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Purchases / Input Tax -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-header bg-light py-3">
|
||||||
|
<h5 class="card-title mb-0 text-success">
|
||||||
|
2. {% trans "Purchases (Input VAT)" %} / المشتريات (ضريبة المدخلات)
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-striped mb-0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="py-3">{% trans "Total Purchases (Excl. VAT)" %} <br> <small class="text-muted">إجمالي المشتريات (غير شامل الضريبة)</small></td>
|
||||||
|
<td class="text-end py-3 fw-bold">{{ total_purchases_subtotal|floatformat:3 }} {{ global_settings.currency_symbol }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-3">{% trans "Total VAT Paid (Recoverable)" %} <br> <small class="text-muted">إجمالي الضريبة المدفوعة (القابلة للاسترداد)</small></td>
|
||||||
|
<td class="text-end py-3 fw-bold text-success">{{ total_input_vat|floatformat:3 }} {{ global_settings.currency_symbol }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-success">
|
||||||
|
<td class="py-3">{% trans "Total Gross Purchases" %} <br> <small class="text-muted">إجمالي المشتريات (شامل الضريبة)</small></td>
|
||||||
|
<td class="text-end py-3 fw-bold">{{ total_purchases_gross|floatformat:3 }} {{ global_settings.currency_symbol }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Net VAT Position -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 shadow-sm bg-primary text-white">
|
||||||
|
<div class="card-body text-center py-4">
|
||||||
|
<h4 class="mb-2">{% trans "Net VAT Payable / (Refundable)" %}</h4>
|
||||||
|
<h5 class="mb-3">صافي الضريبة المستحقة الدفع / (المستردة)</h5>
|
||||||
|
<h1 class="display-4 fw-bold mb-0">
|
||||||
|
{{ net_vat|floatformat:3 }} {{ global_settings.currency_symbol }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 mb-0 opacity-75">
|
||||||
|
{% if net_vat > 0 %}
|
||||||
|
{% trans "Amount to be paid to Tax Authority" %} / المبلغ المستحق للدفع للهيئة الضريبية
|
||||||
|
{% else %}
|
||||||
|
{% trans "Amount to be refunded from Tax Authority" %} / المبلغ المستحق للاسترداد من الهيئة الضريبية
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
3
accounting/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
16
accounting/urls.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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('chart-of-accounts/add/', views.account_create_update, name='account_create'),
|
||||||
|
path('chart-of-accounts/edit/<int:pk>/', views.account_create_update, name='account_edit'),
|
||||||
|
path('journal-entries/', views.journal_entries, name='journal_entries'),
|
||||||
|
path('journal-entries/manual/', views.manual_journal_entry, name='manual_journal_entry'),
|
||||||
|
path('ledger/<int:account_id>/', views.account_ledger, name='account_ledger'),
|
||||||
|
path('reports/vat/', views.vat_report, name='vat_report'),
|
||||||
|
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'),
|
||||||
|
]
|
||||||
297
accounting/views.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from .models import Account, JournalEntry, JournalItem
|
||||||
|
from .forms import AccountForm, JournalEntryForm
|
||||||
|
from core.models import Sale, Purchase, Product
|
||||||
|
from django.db.models import Sum, Q, Value, DecimalField, F
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import datetime, date
|
||||||
|
from django.db import transaction
|
||||||
|
import json
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def vat_report(request):
|
||||||
|
if not (request.user.is_staff or request.user.has_perm('core.view_reports')):
|
||||||
|
messages.error(request, _("You do not have permission to view reports."))
|
||||||
|
return redirect('index')
|
||||||
|
start_date = request.GET.get('start_date')
|
||||||
|
end_date = request.GET.get('end_date')
|
||||||
|
|
||||||
|
if not start_date:
|
||||||
|
start_date = timezone.now().replace(day=1).strftime('%Y-%m-%d')
|
||||||
|
if not end_date:
|
||||||
|
end_date = timezone.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# Convert strings to date objects for filtering
|
||||||
|
# Note: We filter by the day inclusive
|
||||||
|
|
||||||
|
sales = Sale.objects.filter(created_at__date__gte=start_date, created_at__date__lte=end_date).exclude(status='cancelled')
|
||||||
|
purchases = Purchase.objects.filter(created_at__date__gte=start_date, created_at__date__lte=end_date).exclude(status='cancelled').prefetch_related('items__product')
|
||||||
|
|
||||||
|
# Output VAT (Sales)
|
||||||
|
total_sales_subtotal = sales.aggregate(sum=Sum('subtotal'))['sum'] or 0
|
||||||
|
total_output_vat = sales.aggregate(sum=Sum('vat_amount'))['sum'] or 0
|
||||||
|
total_sales_gross = sales.aggregate(sum=Sum('total_amount'))['sum'] or 0
|
||||||
|
|
||||||
|
# Input VAT (Purchases) - Estimated based on Product VAT rate
|
||||||
|
# Since Purchase model doesn't store VAT explicitly, we calculate it from items
|
||||||
|
total_purchases_subtotal = 0
|
||||||
|
total_input_vat = 0
|
||||||
|
|
||||||
|
for purchase in purchases:
|
||||||
|
purchase_vat = 0
|
||||||
|
purchase_subtotal = 0
|
||||||
|
for item in purchase.items.all():
|
||||||
|
# Assume item line_total is cost * quantity
|
||||||
|
# We calculate VAT on top.
|
||||||
|
rate = float(item.product.vat)
|
||||||
|
line_total = float(item.line_total)
|
||||||
|
tax = line_total * (rate / 100.0)
|
||||||
|
purchase_vat += tax
|
||||||
|
purchase_subtotal += line_total
|
||||||
|
|
||||||
|
total_input_vat += purchase_vat
|
||||||
|
total_purchases_subtotal += purchase_subtotal
|
||||||
|
|
||||||
|
total_purchases_gross = total_purchases_subtotal + total_input_vat
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'start_date': start_date,
|
||||||
|
'end_date': end_date,
|
||||||
|
'total_sales_subtotal': total_sales_subtotal,
|
||||||
|
'total_output_vat': total_output_vat,
|
||||||
|
'total_sales_gross': total_sales_gross,
|
||||||
|
'total_purchases_subtotal': total_purchases_subtotal,
|
||||||
|
'total_input_vat': total_input_vat,
|
||||||
|
'total_purchases_gross': total_purchases_gross,
|
||||||
|
'net_vat': float(total_output_vat) - total_input_vat,
|
||||||
|
'currency': 'OMR',
|
||||||
|
}
|
||||||
|
return render(request, 'accounting/vat_report.html', context)
|
||||||
|
|
||||||
|
@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 account_create_update(request, pk=None):
|
||||||
|
if pk:
|
||||||
|
account = get_object_or_404(Account, pk=pk)
|
||||||
|
else:
|
||||||
|
account = None
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = AccountForm(request.POST, instance=account)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Account saved successfully."))
|
||||||
|
return redirect('chart_of_accounts')
|
||||||
|
else:
|
||||||
|
form = AccountForm(instance=account)
|
||||||
|
|
||||||
|
return render(request, 'accounting/account_form.html', {'form': form, 'account': account})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def journal_entries(request):
|
||||||
|
entries = JournalEntry.objects.annotate(
|
||||||
|
total_debit=Coalesce(Sum('items__amount', filter=Q(items__type='debit')), Value(0), output_field=DecimalField()),
|
||||||
|
total_credit=Coalesce(Sum('items__amount', filter=Q(items__type='credit')), Value(0), output_field=DecimalField())
|
||||||
|
).prefetch_related('items__account').order_by('-date', '-id')
|
||||||
|
return render(request, 'accounting/journal_entries.html', {'entries': entries})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def manual_journal_entry(request):
|
||||||
|
accounts = Account.objects.filter(is_active=True).order_by('code')
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = JournalEntryForm(request.POST)
|
||||||
|
# Manual journal entry requires at least two items and they must balance
|
||||||
|
account_ids = request.POST.getlist('account[]')
|
||||||
|
types = request.POST.getlist('type[]')
|
||||||
|
amounts = request.POST.getlist('amount[]')
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
entry = form.save()
|
||||||
|
total_debit = 0
|
||||||
|
total_credit = 0
|
||||||
|
|
||||||
|
for i in range(len(account_ids)):
|
||||||
|
acc_id = account_ids[i]
|
||||||
|
item_type = types[i]
|
||||||
|
amount = float(amounts[i])
|
||||||
|
|
||||||
|
if amount <= 0: continue
|
||||||
|
|
||||||
|
JournalItem.objects.create(
|
||||||
|
entry=entry,
|
||||||
|
account_id=acc_id,
|
||||||
|
type=item_type,
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
|
||||||
|
if item_type == 'debit':
|
||||||
|
total_debit += amount
|
||||||
|
else:
|
||||||
|
total_credit += amount
|
||||||
|
|
||||||
|
if round(total_debit, 3) != round(total_credit, 3):
|
||||||
|
raise Exception(f"Journal entry does not balance. Total Debit: {total_debit}, Total Credit: {total_credit}")
|
||||||
|
|
||||||
|
if total_debit == 0:
|
||||||
|
raise Exception("Journal entry must have at least one debit and one credit.")
|
||||||
|
|
||||||
|
messages.success(request, _("Manual journal entry created successfully."))
|
||||||
|
return redirect('journal_entries')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, str(e))
|
||||||
|
else:
|
||||||
|
form = JournalEntryForm()
|
||||||
|
|
||||||
|
return render(request, 'accounting/journal_entry_form.html', {
|
||||||
|
'form': form,
|
||||||
|
'accounts': accounts
|
||||||
|
})
|
||||||
|
|
||||||
|
@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()
|
||||||
|
})
|
||||||
24
append_reports.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
file_path = 'core/views.py'
|
||||||
|
|
||||||
|
missing_reports = r"""
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def cashflow_report(request):
|
||||||
|
return render(request, 'core/cashflow_report.html')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def customer_statement(request):
|
||||||
|
return render(request, 'core/customer_statement.html')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def supplier_statement(request):
|
||||||
|
return render(request, 'core/supplier_statement.html')
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(file_path, 'a') as f:
|
||||||
|
f.write(missing_reports)
|
||||||
|
|
||||||
|
print("Appended missing reports to core/views.py")
|
||||||
58
apply_patch.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
with open('core/views.py', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
with open('core/patch_views_vat.py', 'r') as f:
|
||||||
|
new_func = f.read()
|
||||||
|
|
||||||
|
# Regex to find the function definition
|
||||||
|
# It starts with @csrf_exempt\ndef create_sale_api(request):
|
||||||
|
# And ends before the next function definition (which likely starts with @ or def)
|
||||||
|
pattern = r"@csrf_exempt\s+def create_sale_api(request):.*?return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)"
|
||||||
|
|
||||||
|
# Note: The pattern needs to match the indentation and multiline content.
|
||||||
|
# Since regex for code blocks is tricky, I will use a simpler approach:
|
||||||
|
# 1. Read the file lines.
|
||||||
|
# 2. Find start line of create_sale_api.
|
||||||
|
# 3. Find the end line (start of next function or end of file).
|
||||||
|
# 4. Replace lines.
|
||||||
|
|
||||||
|
lines = content.splitlines()
|
||||||
|
start_index = -1
|
||||||
|
end_index = -1
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip() == "def create_sale_api(request):":
|
||||||
|
# Check if previous line is decorator
|
||||||
|
if i > 0 and lines[i-1].strip() == "@csrf_exempt":
|
||||||
|
start_index = i - 1
|
||||||
|
else:
|
||||||
|
start_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if start_index != -1:
|
||||||
|
# Find the next function or end
|
||||||
|
# We look for next line starting with 'def ' or '@' at top level
|
||||||
|
for i in range(start_index + 1, len(lines)):
|
||||||
|
if lines[i].startswith("def ") or lines[i].startswith("@"):
|
||||||
|
end_index = i
|
||||||
|
break
|
||||||
|
if end_index == -1:
|
||||||
|
end_index = len(lines)
|
||||||
|
|
||||||
|
# Replace
|
||||||
|
new_lines = new_func.splitlines()
|
||||||
|
# Ensure new lines have correct indentation if needed (but views.py is top level mostly)
|
||||||
|
|
||||||
|
# We need to preserve the imports and structure.
|
||||||
|
# The new_func is complete.
|
||||||
|
|
||||||
|
final_lines = lines[:start_index] + new_lines + lines[end_index:]
|
||||||
|
|
||||||
|
with open('core/views.py', 'w') as f:
|
||||||
|
f.write('\n'.join(final_lines))
|
||||||
|
print("Successfully patched create_sale_api")
|
||||||
|
else:
|
||||||
|
print("Could not find create_sale_api function")
|
||||||
BIN
assets/pasted-20260203-040015-70bc78c5.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/pasted-20260206-043257-ab1cccfb.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/pasted-20260206-044350-8c65cfaa.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/pasted-20260210-082544-8d8f3bac.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/pasted-20260210-182651-b15404c1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/pasted-20260211-124352-68ac57b9.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/pasted-20260211-131304-152989f6.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/pasted-20260211-144653-0ba3e61f.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
assets/vm-shot-2026-02-03T03-59-49-172Z.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
@ -8,9 +8,18 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
env_path = Path(__file__).resolve().parent.parent.parent / '.env'
|
||||||
|
if env_path.exists():
|
||||||
|
load_dotenv(env_path)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
@ -1,50 +1,29 @@
|
|||||||
"""
|
|
||||||
Django settings for config project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 5.2.7.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
load_dotenv(BASE_DIR.parent / ".env")
|
|
||||||
|
|
||||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
|
|
||||||
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
|
||||||
"127.0.0.1",
|
|
||||||
"localhost",
|
|
||||||
os.getenv("HOST_FQDN", ""),
|
|
||||||
]
|
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
|
||||||
origin for origin in [
|
|
||||||
os.getenv("HOST_FQDN", ""),
|
|
||||||
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
|
||||||
] if origin
|
|
||||||
]
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
|
||||||
f"https://{host}" if not host.startswith(("http://", "https://")) else host
|
|
||||||
for host in CSRF_TRUSTED_ORIGINS
|
|
||||||
]
|
|
||||||
|
|
||||||
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
|
|
||||||
SESSION_COOKIE_SECURE = True
|
|
||||||
CSRF_COOKIE_SECURE = True
|
|
||||||
SESSION_COOKIE_SAMESITE = "None"
|
|
||||||
CSRF_COOKIE_SAMESITE = "None"
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-change-me-locally')
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
'https://*.flatlogic.app',
|
||||||
|
'https://*.flatlogic.run',
|
||||||
|
'https://*.flatlogic.com',
|
||||||
|
'http://localhost:8000',
|
||||||
|
'http://127.0.0.1:8000',
|
||||||
|
]
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
@ -56,35 +35,36 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'core',
|
'core',
|
||||||
|
'accounting',
|
||||||
|
'hr',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
|
|
||||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
X_FRAME_OPTIONS = 'ALLOWALL'
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'config.urls'
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [BASE_DIR / 'templates'], # For global templates
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
|
||||||
'core.context_processors.project_context',
|
'core.context_processors.project_context',
|
||||||
|
'core.context_processors.global_settings',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -94,25 +74,22 @@ WSGI_APPLICATION = 'config.wsgi.application'
|
|||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
'NAME': os.getenv('DB_NAME', ''),
|
'NAME': os.environ.get('DB_NAME', 'flatlogic_db'),
|
||||||
'USER': os.getenv('DB_USER', ''),
|
'USER': os.environ.get('DB_USER', 'flatlogic_user'),
|
||||||
'PASSWORD': os.getenv('DB_PASS', ''),
|
'PASSWORD': os.environ.get('DB_PASS', 'flatlogic_password'),
|
||||||
'HOST': os.getenv('DB_HOST', '127.0.0.1'),
|
'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
|
||||||
'PORT': os.getenv('DB_PORT', '3306'),
|
'PORT': os.environ.get('DB_PORT', '3306'),
|
||||||
'OPTIONS': {
|
}
|
||||||
'charset': 'utf8mb4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
@ -131,7 +108,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
@ -141,42 +118,60 @@ USE_I18N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
LANGUAGES = [
|
||||||
|
('en', _('English')),
|
||||||
|
('ar', _('Arabic')),
|
||||||
|
]
|
||||||
|
|
||||||
|
LOCALE_PATHS = [
|
||||||
|
BASE_DIR / 'locale',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / 'static',
|
BASE_DIR / "static",
|
||||||
BASE_DIR / 'assets',
|
BASE_DIR / "assets",
|
||||||
BASE_DIR / 'node_modules',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Email
|
# Conditionally add node_modules if it exists (prevents W004 warning)
|
||||||
EMAIL_BACKEND = os.getenv(
|
if (BASE_DIR / 'node_modules').exists():
|
||||||
"EMAIL_BACKEND",
|
STATICFILES_DIRS.append(BASE_DIR / 'node_modules')
|
||||||
"django.core.mail.backends.smtp.EmailBackend"
|
|
||||||
)
|
|
||||||
EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1")
|
|
||||||
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
|
|
||||||
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
|
|
||||||
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
|
|
||||||
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true"
|
|
||||||
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true"
|
|
||||||
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com")
|
|
||||||
CONTACT_EMAIL_TO = [
|
|
||||||
item.strip()
|
|
||||||
for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",")
|
|
||||||
if item.strip()
|
|
||||||
]
|
|
||||||
|
|
||||||
# When both TLS and SSL flags are enabled, prefer SSL explicitly
|
|
||||||
if EMAIL_USE_SSL:
|
|
||||||
EMAIL_USE_TLS = False
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# Security settings for iframe/proxy support
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
SESSION_COOKIE_SAMESITE = "None"
|
||||||
|
CSRF_COOKIE_SAMESITE = "None"
|
||||||
|
# X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
|
||||||
|
# Email Settings
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com')
|
||||||
|
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
|
||||||
|
EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'True') == 'True'
|
||||||
|
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
|
||||||
|
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
|
||||||
|
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', EMAIL_HOST_USER)
|
||||||
|
CONTACT_EMAIL_TO = os.environ.get('CONTACT_EMAIL_TO', '').split(',')
|
||||||
|
|
||||||
|
# Media files
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
# Authentication Redirects
|
||||||
|
LOGIN_URL = 'login'
|
||||||
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
LOGOUT_REDIRECT_URL = 'login'
|
||||||
|
|||||||
@ -1,29 +1,23 @@
|
|||||||
"""
|
|
||||||
URL configuration for config project.
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from core.helpers import fix_db_view
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
# Emergency Fixer at Root Level (High Priority)
|
||||||
|
path('fix-db/', fix_db_view, name='fix_db_root'),
|
||||||
|
path('fix_db/', fix_db_view, name='fix_db_alias_root'),
|
||||||
|
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
|
path("i18n/", include("django.conf.urls.i18n")),
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
|
path("accounting/", include("accounting.urls")),
|
||||||
|
path("hr/", include("hr.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
@ -4,13 +4,72 @@ WSGI config for config project.
|
|||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
env_path = Path(__file__).resolve().parent.parent.parent / '.env'
|
||||||
|
if env_path.exists():
|
||||||
|
load_dotenv(env_path)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --- FIX: Preload libraries for WeasyPrint/Pango ---
|
||||||
|
# Manually load libraries using absolute paths
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
|
lib_paths = [
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libglib-2.0.so.0',
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0',
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libfontconfig.so.1',
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libcairo.so.2',
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libpango-1.0.so.0',
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libpangoft2-1.0.so.0',
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in lib_paths:
|
||||||
|
try:
|
||||||
|
ctypes.CDLL(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try to import weasyprint
|
||||||
|
try:
|
||||||
|
import weasyprint
|
||||||
|
except Exception as e:
|
||||||
|
# Log error to file and stderr
|
||||||
|
error_msg = f"WeasyPrint Import Error: {str(e)}\n{traceback.format_exc()}"
|
||||||
|
sys.stderr.write(error_msg)
|
||||||
|
try:
|
||||||
|
with open("weasyprint_wsgi_error.log", "w") as f:
|
||||||
|
f.write(error_msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to mock
|
||||||
|
import types
|
||||||
|
class MockHTML:
|
||||||
|
def __init__(self, string=None, base_url=None):
|
||||||
|
pass
|
||||||
|
def write_pdf(self, target=None):
|
||||||
|
raise OSError("WeasyPrint system dependencies are missing. PDF generation is disabled.")
|
||||||
|
|
||||||
|
mock_wp = types.ModuleType("weasyprint")
|
||||||
|
mock_wp.HTML = MockHTML
|
||||||
|
sys.modules["weasyprint"] = mock_wp
|
||||||
|
# ---------------------------------------------------
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
application = get_wsgi_application()
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms_import.cpython-311.pyc
Normal file
BIN
core/__pycache__/helpers.cpython-311.pyc
Normal file
BIN
core/__pycache__/utils.cpython-311.pyc
Normal file
BIN
core/__pycache__/views_import.cpython-311.pyc
Normal file
101
core/admin.py
@ -1,3 +1,102 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import Device
|
||||||
|
from .models import (
|
||||||
|
Category, Unit, Product, Customer, Supplier,
|
||||||
|
Sale, SaleItem, SalePayment,
|
||||||
|
Purchase, PurchaseItem, PurchasePayment,
|
||||||
|
Quotation, QuotationItem,
|
||||||
|
SaleReturn, SaleReturnItem,
|
||||||
|
PurchaseReturn, PurchaseReturnItem,
|
||||||
|
SystemSetting, PaymentMethod, HeldSale,
|
||||||
|
LoyaltyTier, LoyaltyTransaction,
|
||||||
|
CashierCounterRegistry
|
||||||
|
)
|
||||||
|
|
||||||
# Register your models here.
|
@admin.register(Category)
|
||||||
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name_en', 'name_ar', 'slug')
|
||||||
|
prepopulated_fields = {'slug': ('name_en',)}
|
||||||
|
|
||||||
|
@admin.register(Unit)
|
||||||
|
class UnitAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name_en', 'name_ar', 'short_name')
|
||||||
|
|
||||||
|
@admin.register(Product)
|
||||||
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category', 'unit')
|
||||||
|
list_filter = ('category', 'unit', 'is_active')
|
||||||
|
search_fields = ('name_en', 'name_ar', 'sku')
|
||||||
|
|
||||||
|
@admin.register(Customer)
|
||||||
|
class CustomerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'phone', 'email', 'loyalty_points', 'loyalty_tier')
|
||||||
|
search_fields = ('name', 'phone')
|
||||||
|
|
||||||
|
@admin.register(Supplier)
|
||||||
|
class SupplierAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'contact_person', 'phone')
|
||||||
|
|
||||||
|
@admin.register(PaymentMethod)
|
||||||
|
class PaymentMethodAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name_en', 'name_ar', 'is_active')
|
||||||
|
|
||||||
|
class SaleItemInline(admin.TabularInline):
|
||||||
|
model = SaleItem
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class SalePaymentInline(admin.TabularInline):
|
||||||
|
model = SalePayment
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(Sale)
|
||||||
|
class SaleAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'invoice_number', 'customer', 'total_amount', 'paid_amount', 'status', 'created_at')
|
||||||
|
list_filter = ('status', 'created_at')
|
||||||
|
inlines = [SaleItemInline, SalePaymentInline]
|
||||||
|
|
||||||
|
@admin.register(Purchase)
|
||||||
|
class PurchaseAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'invoice_number', 'supplier', 'total_amount', 'paid_amount', 'status', 'created_at')
|
||||||
|
list_filter = ('supplier', 'status', 'created_at')
|
||||||
|
|
||||||
|
@admin.register(Quotation)
|
||||||
|
class QuotationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('quotation_number', 'customer', 'total_amount', 'status', 'created_at')
|
||||||
|
list_filter = ('status', 'created_at')
|
||||||
|
|
||||||
|
@admin.register(SaleReturn)
|
||||||
|
class SaleReturnAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('return_number', 'customer', 'total_amount', 'created_at')
|
||||||
|
|
||||||
|
@admin.register(PurchaseReturn)
|
||||||
|
class PurchaseReturnAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('return_number', 'supplier', 'total_amount', 'created_at')
|
||||||
|
|
||||||
|
@admin.register(SystemSetting)
|
||||||
|
class SystemSettingAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('business_name', 'phone', 'email', 'allow_zero_stock_sales', 'loyalty_enabled', 'wablas_enabled')
|
||||||
|
|
||||||
|
@admin.register(HeldSale)
|
||||||
|
class HeldSaleAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'customer_name', 'created_at')
|
||||||
|
|
||||||
|
@admin.register(LoyaltyTier)
|
||||||
|
class LoyaltyTierAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name_en', 'name_ar', 'min_points', 'point_multiplier', 'discount_percentage')
|
||||||
|
|
||||||
|
@admin.register(LoyaltyTransaction)
|
||||||
|
class LoyaltyTransactionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('customer', 'transaction_type', 'points', 'created_at')
|
||||||
|
list_filter = ('transaction_type', 'created_at')
|
||||||
|
search_fields = ('customer__name',)
|
||||||
|
|
||||||
|
@admin.register(Device)
|
||||||
|
class DeviceAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'device_type', 'connection_type', 'ip_address', 'is_active')
|
||||||
|
list_filter = ('device_type', 'connection_type', 'is_active')
|
||||||
|
search_fields = ('name', 'ip_address')
|
||||||
|
|
||||||
|
@admin.register(CashierCounterRegistry)
|
||||||
|
class CashierCounterRegistryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('cashier', 'counter', 'assigned_at')
|
||||||
|
search_fields = ('cashier__username', 'cashier__first_name', 'counter__name')
|
||||||
@ -1,13 +1,40 @@
|
|||||||
|
from .models import SystemSetting
|
||||||
|
from django.db.utils import OperationalError
|
||||||
|
from django.core.management import call_command
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STARTUP_TIMESTAMP = int(time.time())
|
||||||
|
|
||||||
def project_context(request):
|
def project_context(request):
|
||||||
"""
|
|
||||||
Adds project-specific environment variables to the template context globally.
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
# Used for cache-busting static assets
|
"deployment_timestamp": STARTUP_TIMESTAMP,
|
||||||
"deployment_timestamp": int(time.time()),
|
}
|
||||||
|
|
||||||
|
def global_settings(request):
|
||||||
|
settings = None
|
||||||
|
try:
|
||||||
|
# Use a quick query to avoid hangs if DB is locked
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
if not settings:
|
||||||
|
# Only attempt creation if we are absolutely sure it's missing
|
||||||
|
# and wrap it in a try-except to avoid crashes on read-only DBs
|
||||||
|
try:
|
||||||
|
settings = SystemSetting.objects.create(business_name="Meezan Accounting")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create SystemSetting: {e}")
|
||||||
|
settings = None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Database error in global_settings: {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'site_settings': settings,
|
||||||
|
'global_settings': settings,
|
||||||
|
'decimal_places': settings.decimal_places if settings else 3
|
||||||
}
|
}
|
||||||
|
|||||||
35
core/edit_product_fixed.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@login_required
|
||||||
|
def edit_product(request, pk):
|
||||||
|
product = get_object_or_404(Product, pk=pk)
|
||||||
|
if request.method == 'POST':
|
||||||
|
product.name_en = request.POST.get('name_en')
|
||||||
|
product.name_ar = request.POST.get('name_ar')
|
||||||
|
product.sku = request.POST.get('sku')
|
||||||
|
product.category = get_object_or_404(Category, id=request.POST.get('category'))
|
||||||
|
|
||||||
|
unit_id = request.POST.get('unit')
|
||||||
|
product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None
|
||||||
|
|
||||||
|
supplier_id = request.POST.get('supplier')
|
||||||
|
product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None
|
||||||
|
|
||||||
|
product.cost_price = request.POST.get('cost_price', 0)
|
||||||
|
product.price = request.POST.get('price', 0)
|
||||||
|
product.vat = request.POST.get('vat', 0)
|
||||||
|
product.description = request.POST.get('description', '')
|
||||||
|
product.opening_stock = request.POST.get('opening_stock', 0)
|
||||||
|
product.stock_quantity = request.POST.get('stock_quantity', 0)
|
||||||
|
product.min_stock_level = request.POST.get('min_stock_level', 0)
|
||||||
|
product.is_active = request.POST.get('is_active') == 'on'
|
||||||
|
product.has_expiry = request.POST.get('has_expiry') == 'on'
|
||||||
|
product.expiry_date = request.POST.get('expiry_date')
|
||||||
|
if not product.has_expiry:
|
||||||
|
product.expiry_date = None
|
||||||
|
|
||||||
|
if 'image' in request.FILES:
|
||||||
|
product.image = request.FILES['image']
|
||||||
|
|
||||||
|
product.save()
|
||||||
|
messages.success(request, _("Product updated successfully!"))
|
||||||
|
return redirect(reverse('inventory') + '#items')
|
||||||
|
return redirect(reverse('inventory') + '#items')
|
||||||
28
core/fix_db_view.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Internal Helper Script - NOT for production use
|
||||||
|
from django.db import connection, transaction
|
||||||
|
from core.models import Product
|
||||||
|
|
||||||
|
def fix_missing_columns():
|
||||||
|
"""
|
||||||
|
Manually checks and adds missing columns if migrations fail.
|
||||||
|
"""
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# Check is_service
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT is_service FROM core_product LIMIT 1")
|
||||||
|
except Exception:
|
||||||
|
print("Adding is_service column...")
|
||||||
|
try:
|
||||||
|
cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding column: {e}")
|
||||||
|
|
||||||
|
# Check is_active on PaymentMethod
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1")
|
||||||
|
except Exception:
|
||||||
|
print("Adding is_active column to PaymentMethod...")
|
||||||
|
try:
|
||||||
|
cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding column: {e}")
|
||||||
34
core/fix_view.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
def fix_admin(request):
|
||||||
|
# Keep the old diagnostic view just in case
|
||||||
|
return auto_login_admin(request)
|
||||||
|
|
||||||
|
def auto_login_admin(request):
|
||||||
|
logs = []
|
||||||
|
try:
|
||||||
|
# Get or create admin user
|
||||||
|
user, created = User.objects.get_or_create(username='admin')
|
||||||
|
|
||||||
|
# Force set password
|
||||||
|
user.set_password('admin')
|
||||||
|
|
||||||
|
# Ensure permissions
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
user.is_active = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Log the user in directly
|
||||||
|
user.backend = 'django.contrib.auth.backends.ModelBackend'
|
||||||
|
login(request, user)
|
||||||
|
|
||||||
|
# Redirect to dashboard
|
||||||
|
return HttpResponseRedirect('/')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return HttpResponse(f"<h1>Error Logging In</h1><pre>{e}</pre>")
|
||||||
76
core/forms.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import CashierSession, SystemSetting, Product, Category, Unit, Supplier, Customer, Expense, ExpenseCategory
|
||||||
|
|
||||||
|
class CashierSessionStartForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CashierSession
|
||||||
|
fields = ['opening_balance', 'notes']
|
||||||
|
widgets = {
|
||||||
|
'opening_balance': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||||
|
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
}
|
||||||
|
|
||||||
|
class CashierSessionCloseForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CashierSession
|
||||||
|
fields = ['closing_balance', 'notes']
|
||||||
|
widgets = {
|
||||||
|
'closing_balance': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||||
|
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemSettingForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = SystemSetting
|
||||||
|
fields = '__all__'
|
||||||
|
widgets = {
|
||||||
|
'business_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||||
|
'currency_symbol': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'tax_rate': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||||
|
'vat_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'registration_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'points_per_currency': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||||
|
'currency_per_point': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||||
|
'min_points_to_redeem': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
|
'wablas_token': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'wablas_server_url': forms.URLInput(attrs={'class': 'form-control'}),
|
||||||
|
'wablas_secret_key': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class CategoryForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class UnitForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Unit
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class SupplierForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Supplier
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class CustomerForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Customer
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ExpenseForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Expense
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ExpenseCategoryForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ExpenseCategory
|
||||||
|
fields = '__all__'
|
||||||
4
core/forms_import.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
class ImportFileForm(forms.Form):
|
||||||
|
file = forms.FileField(label="Excel File (.xlsx)")
|
||||||
11
core/helpers.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.http import HttpResponse
|
||||||
|
from django.core.management import call_command
|
||||||
|
import io
|
||||||
|
|
||||||
|
def fix_db_view(request):
|
||||||
|
out = io.StringIO()
|
||||||
|
try:
|
||||||
|
call_command('migrate', 'core', stdout=out)
|
||||||
|
return HttpResponse(f"SUCCESS: Database updated.<br><pre>{out.getvalue()}</pre><br><a href='/'>Go Home</a>")
|
||||||
|
except Exception as e:
|
||||||
|
return HttpResponse(f"ERROR: {e}<br><pre>{out.getvalue()}</pre>")
|
||||||
91
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 06:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=100, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=100, verbose_name='Name (Arabic)')),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Categories',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Customer',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')),
|
||||||
|
('address', models.TextField(blank=True, verbose_name='Address')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Supplier',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||||
|
('contact_person', models.CharField(blank=True, max_length=200, verbose_name='Contact Person')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=200, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=200, verbose_name='Name (Arabic)')),
|
||||||
|
('sku', models.CharField(max_length=50, unique=True, verbose_name='SKU')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||||
|
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
|
||||||
|
('stock_quantity', models.PositiveIntegerField(default=0, verbose_name='Stock Quantity')),
|
||||||
|
('image', models.URLField(blank=True, null=True, verbose_name='Product Image')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.category')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Sale',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||||
|
('discount', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.customer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SaleItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField()),
|
||||||
|
('unit_price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('line_total', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.sale')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Purchase',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('supplier', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.supplier')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
26
core/migrations/0002_systemsetting.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 07:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SystemSetting',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('business_name', models.CharField(default='Meezan Accounting', max_length=200, verbose_name='Business Name')),
|
||||||
|
('address', models.TextField(blank=True, verbose_name='Address')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')),
|
||||||
|
('currency_symbol', models.CharField(default='$', max_length=10, verbose_name='Currency Symbol')),
|
||||||
|
('tax_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Tax Rate (%)')),
|
||||||
|
('logo_url', models.URLField(blank=True, null=True, verbose_name='Logo URL')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 07:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_systemsetting'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Modified to handle inconsistent database state (column already missing)
|
||||||
|
migrations.SeparateDatabaseAndState(
|
||||||
|
state_operations=[
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='logo_url',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
database_operations=[
|
||||||
|
# Intentionally empty to skip SQL execution.
|
||||||
|
# The column 'logo_url' is likely already missing in the DB, causing 1091 errors.
|
||||||
|
# In fresh installs, this leaves a zombie column, which is harmless as it's not in the model.
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='logo',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='business_logos/', verbose_name='Logo'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='registration_number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Registration Number'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='vat_number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='VAT Number'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='price',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Price'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='total_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sale',
|
||||||
|
name='discount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sale',
|
||||||
|
name='total_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='line_total',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='unit_price',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=12),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='currency_symbol',
|
||||||
|
field=models.CharField(default='OMR', max_length=10, verbose_name='Currency Symbol'),
|
||||||
|
),
|
||||||
|
]
|
||||||
28
core/migrations/0004_unit_product_unit.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 08:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_remove_systemsetting_logo_url_systemsetting_logo_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Unit',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=50, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')),
|
||||||
|
('short_name', models.CharField(max_length=10, verbose_name='Short Name')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='unit',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.unit'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 08:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_unit_product_unit'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='cost_price',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='Cost Price'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Active'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='opening_stock',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='Opening Stock'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='supplier',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.supplier'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='vat',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='VAT (%)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='image',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='product_images/', verbose_name='Product Image'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='price',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Sale Price'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='sku',
|
||||||
|
field=models.CharField(max_length=50, unique=True, verbose_name='Barcode/SKU'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='stock_quantity',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='In Stock'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 08:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_product_cost_price_product_is_active_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='balance_due',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Balance Due'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='due_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Due Date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='invoice_number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Invoice Number'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='paid_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Paid Amount'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='payment_type',
|
||||||
|
field=models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit'), ('partial', 'Partial')], default='cash', max_length=20, verbose_name='Payment Type'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('paid', 'Paid'), ('partial', 'Partial'), ('unpaid', 'Unpaid')], default='paid', max_length=20, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='supplier',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchases', to='core.supplier'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='total_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchaseItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
|
||||||
|
('cost_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Cost Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('purchase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.purchase')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchasePayment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')),
|
||||||
|
('payment_date', models.DateField(default=django.utils.timezone.now, verbose_name='Payment Date')),
|
||||||
|
('payment_method', models.CharField(default='Cash', max_length=50, verbose_name='Payment Method')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('purchase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='core.purchase')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 09:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0006_purchase_balance_due_purchase_due_date_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='balance_due',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Balance Due'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='due_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Due Date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='invoice_number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Invoice Number'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='paid_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Paid Amount'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='payment_type',
|
||||||
|
field=models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit'), ('partial', 'Partial')], default='cash', max_length=20, verbose_name='Payment Type'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('paid', 'Paid'), ('partial', 'Partial'), ('unpaid', 'Unpaid')], default='paid', max_length=20, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sale',
|
||||||
|
name='customer',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to='core.customer'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sale',
|
||||||
|
name='discount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Discount'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sale',
|
||||||
|
name='total_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='line_total',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.PositiveIntegerField(verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='unit_price',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SalePayment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')),
|
||||||
|
('payment_date', models.DateField(default=django.utils.timezone.now, verbose_name='Payment Date')),
|
||||||
|
('payment_method', models.CharField(default='Cash', max_length=50, verbose_name='Payment Method')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='core.sale')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 09:49
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Quotation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quotation_number', models.CharField(blank=True, max_length=50, verbose_name='Quotation Number')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
|
||||||
|
('discount', models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Discount')),
|
||||||
|
('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('converted', 'Converted to Invoice')], default='draft', max_length=20, verbose_name='Status')),
|
||||||
|
('valid_until', models.DateField(blank=True, null=True, verbose_name='Valid Until')),
|
||||||
|
('terms_and_conditions', models.TextField(blank=True, verbose_name='Terms and Conditions')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotations', to='core.customer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='quotation',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted_sale', to='core.quotation'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuotationItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
|
||||||
|
('unit_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('quotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.quotation')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 10:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0008_quotation_sale_quotation_quotationitem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchaseReturn',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('return_number', models.CharField(blank=True, max_length=50, verbose_name='Return Number')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('purchase', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='returns', to='core.purchase')),
|
||||||
|
('supplier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_returns', to='core.supplier')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchaseReturnItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
|
||||||
|
('cost_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Cost Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('purchase_return', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.purchasereturn')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SaleReturn',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('return_number', models.CharField(blank=True, max_length=50, verbose_name='Return Number')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_returns', to='core.customer')),
|
||||||
|
('sale', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='returns', to='core.sale')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SaleReturnItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
|
||||||
|
('unit_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('sale_return', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.salereturn')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 10:42
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0009_purchasereturn_purchasereturnitem_salereturn_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchases', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchasepayment',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchasereturn',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_returns', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='quotation',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotations', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salepayment',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_payments', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salereturn',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_returns', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 13:01
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0010_purchase_created_by_purchasepayment_created_by_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PaymentMethod',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=50, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchasepayment',
|
||||||
|
name='payment_method_name',
|
||||||
|
field=models.CharField(default='Cash', max_length=50, verbose_name='Payment Method Name'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salepayment',
|
||||||
|
name='payment_method_name',
|
||||||
|
field=models.CharField(default='Cash', max_length=50, verbose_name='Payment Method Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchasepayment',
|
||||||
|
name='payment_method',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to='core.paymentmethod'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salepayment',
|
||||||
|
name='payment_method',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_payments', to='core.paymentmethod'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0012_systemsetting_decimal_places.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 16:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0011_paymentmethod_purchasepayment_payment_method_name_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='decimal_places',
|
||||||
|
field=models.PositiveSmallIntegerField(default=3, verbose_name='Decimal Places'),
|
||||||
|
),
|
||||||
|
]
|
||||||
28
core/migrations/0013_heldsale.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 16:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0012_systemsetting_decimal_places'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='HeldSale',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('cart_data', models.JSONField(verbose_name='Cart Data')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='held_sales', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='held_sales', to='core.customer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 16:46
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0013_heldsale'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LoyaltyTier',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=50, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')),
|
||||||
|
('min_points', models.PositiveIntegerField(default=0, verbose_name='Minimum Points')),
|
||||||
|
('point_multiplier', models.DecimalField(decimal_places=2, default=1.0, max_digits=4, verbose_name='Point Multiplier')),
|
||||||
|
('discount_percentage', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Discount Percentage')),
|
||||||
|
('color_code', models.CharField(default='#6c757d', max_length=20, verbose_name='Color Code')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customer',
|
||||||
|
name='loyalty_points',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Loyalty Points'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='loyalty_discount_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Loyalty Discount'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='loyalty_points_redeemed',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Loyalty Points Redeemed'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='currency_per_point',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0.01, max_digits=10, verbose_name='Currency Value per Point'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='loyalty_enabled',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Enable Loyalty System'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='min_points_to_redeem',
|
||||||
|
field=models.PositiveIntegerField(default=100, verbose_name='Minimum Points to Redeem'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='points_per_currency',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=10, verbose_name='Points Earned per Currency Unit'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customer',
|
||||||
|
name='loyalty_tier',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customers', to='core.loyaltytier'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LoyaltyTransaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('transaction_type', models.CharField(choices=[('earned', 'Earned'), ('redeemed', 'Redeemed'), ('adjusted', 'Adjusted')], max_length=20, verbose_name='Type')),
|
||||||
|
('points', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Points')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loyalty_transactions', to='core.customer')),
|
||||||
|
('sale', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loyalty_transactions', to='core.sale')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
26
core/migrations/0015_userprofile.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 16:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0014_loyaltytier_customer_loyalty_points_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image', models.FileField(blank=True, null=True, upload_to='profile_pics/', verbose_name='Profile Picture')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone Number')),
|
||||||
|
('bio', models.TextField(blank=True, verbose_name='Bio')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
43
core/migrations/0016_expensecategory_expense.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 17:15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0015_userprofile'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExpenseCategory',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=100, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=100, verbose_name='Name (Arabic)')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Expense Categories',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Expense',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')),
|
||||||
|
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Date')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||||
|
('attachment', models.FileField(blank=True, null=True, upload_to='expense_attachments/', verbose_name='Attachment')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('payment_method', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses', to='core.paymentmethod')),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expenses', to='core.expensecategory')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-03 05:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0017_expensecategory_accounting_account'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='wablas_enabled',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Enable WhatsApp Gateway'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='wablas_server_url',
|
||||||
|
field=models.URLField(blank=True, help_text='Example: https://console.wablas.com', verbose_name='Wablas Server URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='wablas_token',
|
||||||
|
field=models.CharField(blank=True, max_length=255, verbose_name='Wablas API Token'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0019_systemsetting_wablas_secret_key.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-03 05:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0018_systemsetting_wablas_enabled_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='wablas_secret_key',
|
||||||
|
field=models.CharField(blank=True, max_length=255, verbose_name='Wablas Secret Key'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-03 10:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0019_systemsetting_wablas_secret_key'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='expiry_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='has_expiry',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Has Expiry Date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchaseitem',
|
||||||
|
name='expiry_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchasereturnitem',
|
||||||
|
name='expiry_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0021_product_min_stock_level.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-03 10:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0020_product_expiry_date_product_has_expiry_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='min_stock_level',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='Stock Level (Alert)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-03 10:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0021_product_min_stock_level'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='min_stock_level',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Stock Level (Alert)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='opening_stock',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Opening Stock'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='stock_quantity',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='In Stock'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchasereturnitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='quotationitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salereturnitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-03 10:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0022_alter_product_min_stock_level_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='min_stock_level',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Stock Level (Alert)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='opening_stock',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Opening Stock'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='stock_quantity',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='In Stock'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchasereturnitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='quotationitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salereturnitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
core/migrations/0024_device.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-05 12:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0023_alter_product_min_stock_level_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Device',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Device Name')),
|
||||||
|
('device_type', models.CharField(choices=[('printer', 'Printer'), ('scanner', 'Scanner'), ('scale', 'Weight Scale'), ('display', 'Customer Display'), ('other', 'Other')], max_length=20, verbose_name='Device Type')),
|
||||||
|
('connection_type', models.CharField(choices=[('network', 'Network (IP)'), ('usb', 'USB'), ('bluetooth', 'Bluetooth')], default='network', max_length=20, verbose_name='Connection Type')),
|
||||||
|
('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP Address')),
|
||||||
|
('port', models.PositiveIntegerField(blank=True, null=True, verbose_name='Port')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||||
|
('config_json', models.JSONField(blank=True, help_text='Additional driver configuration in JSON format', null=True, verbose_name='Configuration')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
23
core/migrations/0025_sale_subtotal_sale_vat_amount.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-05 12:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0024_device'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='subtotal',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Subtotal'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='vat_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='VAT Amount'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-06 05:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0025_sale_subtotal_sale_vat_amount'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchaseOrder',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('lpo_number', models.CharField(blank=True, max_length=50, verbose_name='LPO Number')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
|
||||||
|
('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent'), ('converted', 'Converted to Purchase'), ('cancelled', 'Cancelled')], default='draft', max_length=20, verbose_name='Status')),
|
||||||
|
('issue_date', models.DateField(default=django.utils.timezone.now, verbose_name='Issue Date')),
|
||||||
|
('expected_date', models.DateField(blank=True, null=True, verbose_name='Expected Date')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_orders', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('supplier', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_orders', to='core.supplier')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='purchase_order',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted_purchase', to='core.purchaseorder'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchaseOrderItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity')),
|
||||||
|
('cost_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Cost Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.purchaseorder')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||