Compare commits

..

No commits in common. "ai-dev" and "master" have entirely different histories.

324 changed files with 301 additions and 38289 deletions

9
.gitignore vendored
View File

@ -1,12 +1,3 @@
node_modules/
*/node_modules/
*/build/
.env
db.sqlite3
media/
staticfiles/
__pycache__/
*.pyc
.DS_Store
*.log
tmp/

View File

@ -1,33 +0,0 @@
# 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"]

View File

View File

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

View File

@ -1,10 +0,0 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class AccountingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounting'
verbose_name = _('Accounting')
def ready(self):
import accounting.signals

View File

@ -1,26 +0,0 @@
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'}),
}

View File

@ -1,47 +0,0 @@
from django.core.management.base import BaseCommand
from accounting.models import Account
class Command(BaseCommand):
help = 'Setup default Chart of Accounts'
def handle(self, *args, **options):
default_accounts = [
# ASSETS
{'code': '1000', 'name_en': 'Cash', 'name_ar': 'النقد', 'account_type': 'asset'},
{'code': '1010', 'name_en': 'Bank', 'name_ar': 'البنك', 'account_type': 'asset'},
{'code': '1200', 'name_en': 'Accounts Receivable', 'name_ar': 'الذمم المدينة', 'account_type': 'asset'},
{'code': '1300', 'name_en': 'Inventory', 'name_ar': 'المخزون', 'account_type': 'asset'},
# LIABILITIES
{'code': '2000', 'name_en': 'Accounts Payable', 'name_ar': 'الذمم الدائنة', 'account_type': 'liability'},
{'code': '2100', 'name_en': 'VAT Payable', 'name_ar': 'ضريبة القيمة المضافة المستحقة', 'account_type': 'liability'},
# EQUITY
{'code': '3000', 'name_en': 'Owner Equity', 'name_ar': 'رأس المال', 'account_type': 'equity'},
{'code': '3100', 'name_en': 'Retained Earnings', 'name_ar': 'الأرباح المحتجزة', 'account_type': 'equity'},
# INCOME
{'code': '4000', 'name_en': 'Sales Revenue', 'name_ar': 'إيرادات المبيعات', 'account_type': 'income'},
{'code': '4100', 'name_en': 'Other Income', 'name_ar': 'إيرادات أخرى', 'account_type': 'income'},
# EXPENSES
{'code': '5000', 'name_en': 'Cost of Goods Sold', 'name_ar': 'تكلفة البضائع المباعة', 'account_type': 'expense'},
{'code': '5100', 'name_en': 'Salaries Expense', 'name_ar': 'مصاريف الرواتب', 'account_type': 'expense'},
{'code': '5200', 'name_en': 'Rent Expense', 'name_ar': 'مصاريف الإيجار', 'account_type': 'expense'},
{'code': '5300', 'name_en': 'Utility Expense', 'name_ar': 'مصاريف المرافق', 'account_type': 'expense'},
{'code': '5400', 'name_en': 'General Expense', 'name_ar': 'مصاريف عامة', 'account_type': 'expense'},
]
for acc_data in default_accounts:
account, created = Account.objects.get_or_create(
code=acc_data['code'],
defaults={
'name_en': acc_data['name_en'],
'name_ar': acc_data['name_ar'],
'account_type': acc_data['account_type']
}
)
if created:
self.stdout.write(self.style.SUCCESS(f'Created account: {account.name_en}'))
else:
self.stdout.write(f'Account already exists: {account.name_en}')

View File

@ -1,56 +0,0 @@
# Generated by Django 5.2.7 on 2026-02-03 03:14
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=20, unique=True, verbose_name='Account Code')),
('name_en', models.CharField(max_length=100, verbose_name='Name (English)')),
('name_ar', models.CharField(max_length=100, verbose_name='Name (Arabic)')),
('account_type', models.CharField(choices=[('asset', 'Asset'), ('liability', 'Liability'), ('equity', 'Equity'), ('income', 'Income'), ('expense', 'Expense')], max_length=20, verbose_name='Account Type')),
('description', models.TextField(blank=True, verbose_name='Description')),
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='JournalEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Date')),
('description', models.TextField(verbose_name='Description')),
('reference', models.CharField(blank=True, max_length=100, verbose_name='Reference')),
('object_id', models.PositiveIntegerField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
],
options={
'verbose_name_plural': 'Journal Entries',
},
),
migrations.CreateModel(
name='JournalItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('debit', 'Debit'), ('credit', 'Credit')], max_length=10, verbose_name='Type')),
('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')),
('notes', models.CharField(blank=True, max_length=255, verbose_name='Notes')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='journal_items', to='accounting.account')),
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='accounting.journalentry')),
],
),
]

View File

@ -1,73 +0,0 @@
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}"

View File

@ -1,167 +0,0 @@
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()

View File

@ -1,67 +0,0 @@
{% 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 %}

View File

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

View File

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

View File

@ -1,65 +0,0 @@
{% 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 %}

View File

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

View File

@ -1,104 +0,0 @@
{% 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 %}

View File

@ -1,202 +0,0 @@
{% 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 %}

View File

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

View File

@ -1,66 +0,0 @@
{% 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 %}

View File

@ -1,121 +0,0 @@
{% 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 %}

View File

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

View File

@ -1,16 +0,0 @@
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'),
]

View File

@ -1,297 +0,0 @@
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()
})

View File

@ -1,24 +0,0 @@
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")

View File

@ -1,58 +0,0 @@
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")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -8,18 +8,9 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
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
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application()
application = get_asgi_application()

View File

@ -1,29 +1,50 @@
import os
import sys
"""
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
from django.utils.translation import gettext_lazy as _
import os
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR.parent / ".env")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
# 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 = ['*']
ALLOWED_HOSTS = [
"127.0.0.1",
"localhost",
os.getenv("HOST_FQDN", ""),
]
CSRF_TRUSTED_ORIGINS = [
'https://*.flatlogic.app',
'https://*.flatlogic.run',
'https://*.flatlogic.com',
'http://localhost:8000',
'http://127.0.0.1:8000',
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
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# Application definition
@ -35,36 +56,35 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
'accounting',
'hr',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
X_FRAME_OPTIONS = 'ALLOWALL'
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # For global templates
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'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.global_settings',
],
},
},
@ -74,22 +94,25 @@ WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DB_NAME', 'flatlogic_db'),
'USER': os.environ.get('DB_USER', 'flatlogic_user'),
'PASSWORD': os.environ.get('DB_PASS', 'flatlogic_password'),
'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
'PORT': os.environ.get('DB_PORT', '3306'),
}
'NAME': os.getenv('DB_NAME', ''),
'USER': os.getenv('DB_USER', ''),
'PASSWORD': os.getenv('DB_PASS', ''),
'HOST': os.getenv('DB_HOST', '127.0.0.1'),
'PORT': os.getenv('DB_PORT', '3306'),
'OPTIONS': {
'charset': 'utf8mb4',
},
},
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
@ -108,7 +131,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
@ -118,60 +141,42 @@ USE_I18N = True
USE_TZ = True
LANGUAGES = [
('en', _('English')),
('ar', _('Arabic')),
]
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / "static",
BASE_DIR / "assets",
BASE_DIR / 'static',
BASE_DIR / 'assets',
BASE_DIR / 'node_modules',
]
# Conditionally add node_modules if it exists (prevents W004 warning)
if (BASE_DIR / 'node_modules').exists():
STATICFILES_DIRS.append(BASE_DIR / 'node_modules')
# Email
EMAIL_BACKEND = os.getenv(
"EMAIL_BACKEND",
"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
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
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'

View File

@ -1,23 +1,29 @@
"""
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.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
from core.helpers import fix_db_view
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("accounts/", include("django.contrib.auth.urls")),
path("i18n/", include("django.conf.urls.i18n")),
path("", include("core.urls")),
path("accounting/", include("accounting.urls")),
path("hr/", include("hr.urls")),
]
if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -4,72 +4,13 @@ WSGI config for config project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from pathlib import Path
import ctypes
import ctypes.util
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')
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()

View File

@ -1,102 +1,3 @@
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
)
@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')
# Register your models here.

View File

@ -1,40 +1,13 @@
from .models import SystemSetting
from django.db.utils import OperationalError
from django.core.management import call_command
import os
import time
import logging
logger = logging.getLogger(__name__)
STARTUP_TIMESTAMP = int(time.time())
def project_context(request):
"""
Adds project-specific environment variables to the template context globally.
"""
return {
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
"deployment_timestamp": STARTUP_TIMESTAMP,
}
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
# Used for cache-busting static assets
"deployment_timestamp": int(time.time()),
}

View File

@ -1,35 +0,0 @@
@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')

View File

@ -1,28 +0,0 @@
# 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}")

View File

@ -1,34 +0,0 @@
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>")

View File

@ -1,76 +0,0 @@
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__'

View File

@ -1,4 +0,0 @@
from django import forms
class ImportFileForm(forms.Form):
file = forms.FileField(label="Excel File (.xlsx)")

View File

@ -1,11 +0,0 @@
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>")

View File

@ -1,91 +0,0 @@
# 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')),
],
),
]

View File

@ -1,26 +0,0 @@
# 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')),
],
),
]

View File

@ -1,77 +0,0 @@
# 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'),
),
]

View File

@ -1,28 +0,0 @@
# 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'),
),
]

View File

@ -1,59 +0,0 @@
# 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'),
),
]

View File

@ -1,82 +0,0 @@
# 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')),
],
),
]

View File

@ -1,91 +0,0 @@
# 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')),
],
),
]

View File

@ -1,45 +0,0 @@
# 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')),
],
),
]

View File

@ -1,60 +0,0 @@
# 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')),
],
),
]

View File

@ -1,51 +0,0 @@
# 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),
),
]

View File

@ -1,43 +0,0 @@
# 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'),
),
]

View File

@ -1,18 +0,0 @@
# 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'),
),
]

View File

@ -1,28 +0,0 @@
# 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')),
],
),
]

View File

@ -1,78 +0,0 @@
# 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')),
],
),
]

View File

@ -1,26 +0,0 @@
# 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)),
],
),
]

View File

@ -1,43 +0,0 @@
# 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')),
],
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 5.2.7 on 2026-02-03 03:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounting', '0001_initial'),
('core', '0016_expensecategory_expense'),
]
operations = [
migrations.AddField(
model_name='expensecategory',
name='accounting_account',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expense_categories', to='accounting.account'),
),
]

View File

@ -1,28 +0,0 @@
# 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'),
),
]

View File

@ -1,18 +0,0 @@
# 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'),
),
]

View File

@ -1,33 +0,0 @@
# 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'),
),
]

View File

@ -1,18 +0,0 @@
# 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)'),
),
]

View File

@ -1,53 +0,0 @@
# 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'),
),
]

View File

@ -1,53 +0,0 @@
# 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'),
),
]

View File

@ -1,27 +0,0 @@
# 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)),
],
),
]

View File

@ -1,23 +0,0 @@
# 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'),
),
]

View File

@ -1,48 +0,0 @@
# 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')),
],
),
]

Some files were not shown because too many files have changed in this diff Show More