Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
9
.gitignore
vendored
@ -1,12 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*/node_modules/
|
*/node_modules/
|
||||||
*/build/
|
*/build/
|
||||||
.env
|
|
||||||
db.sqlite3
|
|
||||||
media/
|
|
||||||
staticfiles/
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
.DS_Store
|
|
||||||
*.log
|
|
||||||
tmp/
|
|
||||||
|
|||||||
33
Dockerfile
@ -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"]
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
@ -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
|
|
||||||
@ -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'}),
|
|
||||||
}
|
|
||||||
@ -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}')
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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}"
|
|
||||||
@ -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()
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@ -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'),
|
|
||||||
]
|
|
||||||
@ -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()
|
|
||||||
})
|
|
||||||
@ -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")
|
|
||||||
@ -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")
|
|
||||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 31 KiB |
@ -8,18 +8,9 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
try:
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
env_path = Path(__file__).resolve().parent.parent.parent / '.env'
|
|
||||||
if env_path.exists():
|
|
||||||
load_dotenv(env_path)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
|
|||||||
@ -1,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 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
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
load_dotenv(BASE_DIR.parent / ".env")
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
|
||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
ALLOWED_HOSTS = [
|
||||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-change-me-locally')
|
"127.0.0.1",
|
||||||
|
"localhost",
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
os.getenv("HOST_FQDN", ""),
|
||||||
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
|
]
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
'https://*.flatlogic.app',
|
origin for origin in [
|
||||||
'https://*.flatlogic.run',
|
os.getenv("HOST_FQDN", ""),
|
||||||
'https://*.flatlogic.com',
|
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
||||||
'http://localhost:8000',
|
] if origin
|
||||||
'http://127.0.0.1:8000',
|
|
||||||
]
|
]
|
||||||
|
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
|
# Application definition
|
||||||
|
|
||||||
@ -35,36 +56,35 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'core',
|
'core',
|
||||||
'accounting',
|
|
||||||
'hr',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
|
||||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
X_FRAME_OPTIONS = 'ALLOWALL'
|
||||||
|
|
||||||
ROOT_URLCONF = 'config.urls'
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [BASE_DIR / 'templates'], # For global templates
|
'DIRS': [],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
||||||
'core.context_processors.project_context',
|
'core.context_processors.project_context',
|
||||||
'core.context_processors.global_settings',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -74,22 +94,25 @@ WSGI_APPLICATION = 'config.wsgi.application'
|
|||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
'NAME': os.environ.get('DB_NAME', 'flatlogic_db'),
|
'NAME': os.getenv('DB_NAME', ''),
|
||||||
'USER': os.environ.get('DB_USER', 'flatlogic_user'),
|
'USER': os.getenv('DB_USER', ''),
|
||||||
'PASSWORD': os.environ.get('DB_PASS', 'flatlogic_password'),
|
'PASSWORD': os.getenv('DB_PASS', ''),
|
||||||
'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
|
'HOST': os.getenv('DB_HOST', '127.0.0.1'),
|
||||||
'PORT': os.environ.get('DB_PORT', '3306'),
|
'PORT': os.getenv('DB_PORT', '3306'),
|
||||||
}
|
'OPTIONS': {
|
||||||
|
'charset': 'utf8mb4',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# 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 = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
@ -108,7 +131,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
@ -118,60 +141,42 @@ USE_I18N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
LANGUAGES = [
|
|
||||||
('en', _('English')),
|
|
||||||
('ar', _('Arabic')),
|
|
||||||
]
|
|
||||||
|
|
||||||
LOCALE_PATHS = [
|
|
||||||
BASE_DIR / 'locale',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / "static",
|
BASE_DIR / 'static',
|
||||||
BASE_DIR / "assets",
|
BASE_DIR / 'assets',
|
||||||
|
BASE_DIR / 'node_modules',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Conditionally add node_modules if it exists (prevents W004 warning)
|
# Email
|
||||||
if (BASE_DIR / 'node_modules').exists():
|
EMAIL_BACKEND = os.getenv(
|
||||||
STATICFILES_DIRS.append(BASE_DIR / 'node_modules')
|
"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
|
# 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'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
# Security settings for iframe/proxy support
|
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
|
||||||
SESSION_COOKIE_SECURE = True
|
|
||||||
CSRF_COOKIE_SECURE = True
|
|
||||||
SESSION_COOKIE_SAMESITE = "None"
|
|
||||||
CSRF_COOKIE_SAMESITE = "None"
|
|
||||||
# X_FRAME_OPTIONS = 'SAMEORIGIN'
|
|
||||||
|
|
||||||
# Email Settings
|
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
|
||||||
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com')
|
|
||||||
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
|
|
||||||
EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'True') == 'True'
|
|
||||||
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
|
|
||||||
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
|
|
||||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', EMAIL_HOST_USER)
|
|
||||||
CONTACT_EMAIL_TO = os.environ.get('CONTACT_EMAIL_TO', '').split(',')
|
|
||||||
|
|
||||||
# Media files
|
|
||||||
MEDIA_URL = '/media/'
|
|
||||||
MEDIA_ROOT = BASE_DIR / 'media'
|
|
||||||
|
|
||||||
# Authentication Redirects
|
|
||||||
LOGIN_URL = 'login'
|
|
||||||
LOGIN_REDIRECT_URL = '/'
|
|
||||||
LOGOUT_REDIRECT_URL = 'login'
|
|
||||||
|
|||||||
@ -1,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.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from core.helpers import fix_db_view
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Emergency Fixer at Root Level (High Priority)
|
|
||||||
path('fix-db/', fix_db_view, name='fix_db_root'),
|
|
||||||
path('fix_db/', fix_db_view, name='fix_db_alias_root'),
|
|
||||||
|
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("accounts/", include("django.contrib.auth.urls")),
|
|
||||||
path("i18n/", include("django.conf.urls.i18n")),
|
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
path("accounting/", include("accounting.urls")),
|
|
||||||
path("hr/", include("hr.urls")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
||||||
|
|||||||
@ -4,72 +4,13 @@ WSGI config for config project.
|
|||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
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
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
application = get_wsgi_application()
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
|
|||||||
101
core/admin.py
@ -1,102 +1,3 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Device
|
|
||||||
from .models import (
|
|
||||||
Category, Unit, Product, Customer, Supplier,
|
|
||||||
Sale, SaleItem, SalePayment,
|
|
||||||
Purchase, PurchaseItem, PurchasePayment,
|
|
||||||
Quotation, QuotationItem,
|
|
||||||
SaleReturn, SaleReturnItem,
|
|
||||||
PurchaseReturn, PurchaseReturnItem,
|
|
||||||
SystemSetting, PaymentMethod, HeldSale,
|
|
||||||
LoyaltyTier, LoyaltyTransaction,
|
|
||||||
CashierCounterRegistry
|
|
||||||
)
|
|
||||||
|
|
||||||
@admin.register(Category)
|
# Register your models here.
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name_en', 'name_ar', 'slug')
|
|
||||||
prepopulated_fields = {'slug': ('name_en',)}
|
|
||||||
|
|
||||||
@admin.register(Unit)
|
|
||||||
class UnitAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name_en', 'name_ar', 'short_name')
|
|
||||||
|
|
||||||
@admin.register(Product)
|
|
||||||
class ProductAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category', 'unit')
|
|
||||||
list_filter = ('category', 'unit', 'is_active')
|
|
||||||
search_fields = ('name_en', 'name_ar', 'sku')
|
|
||||||
|
|
||||||
@admin.register(Customer)
|
|
||||||
class CustomerAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'phone', 'email', 'loyalty_points', 'loyalty_tier')
|
|
||||||
search_fields = ('name', 'phone')
|
|
||||||
|
|
||||||
@admin.register(Supplier)
|
|
||||||
class SupplierAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'contact_person', 'phone')
|
|
||||||
|
|
||||||
@admin.register(PaymentMethod)
|
|
||||||
class PaymentMethodAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name_en', 'name_ar', 'is_active')
|
|
||||||
|
|
||||||
class SaleItemInline(admin.TabularInline):
|
|
||||||
model = SaleItem
|
|
||||||
extra = 1
|
|
||||||
|
|
||||||
class SalePaymentInline(admin.TabularInline):
|
|
||||||
model = SalePayment
|
|
||||||
extra = 1
|
|
||||||
|
|
||||||
@admin.register(Sale)
|
|
||||||
class SaleAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('id', 'invoice_number', 'customer', 'total_amount', 'paid_amount', 'status', 'created_at')
|
|
||||||
list_filter = ('status', 'created_at')
|
|
||||||
inlines = [SaleItemInline, SalePaymentInline]
|
|
||||||
|
|
||||||
@admin.register(Purchase)
|
|
||||||
class PurchaseAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('id', 'invoice_number', 'supplier', 'total_amount', 'paid_amount', 'status', 'created_at')
|
|
||||||
list_filter = ('supplier', 'status', 'created_at')
|
|
||||||
|
|
||||||
@admin.register(Quotation)
|
|
||||||
class QuotationAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('quotation_number', 'customer', 'total_amount', 'status', 'created_at')
|
|
||||||
list_filter = ('status', 'created_at')
|
|
||||||
|
|
||||||
@admin.register(SaleReturn)
|
|
||||||
class SaleReturnAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('return_number', 'customer', 'total_amount', 'created_at')
|
|
||||||
|
|
||||||
@admin.register(PurchaseReturn)
|
|
||||||
class PurchaseReturnAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('return_number', 'supplier', 'total_amount', 'created_at')
|
|
||||||
|
|
||||||
@admin.register(SystemSetting)
|
|
||||||
class SystemSettingAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('business_name', 'phone', 'email', 'allow_zero_stock_sales', 'loyalty_enabled', 'wablas_enabled')
|
|
||||||
|
|
||||||
@admin.register(HeldSale)
|
|
||||||
class HeldSaleAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('id', 'customer_name', 'created_at')
|
|
||||||
|
|
||||||
@admin.register(LoyaltyTier)
|
|
||||||
class LoyaltyTierAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name_en', 'name_ar', 'min_points', 'point_multiplier', 'discount_percentage')
|
|
||||||
|
|
||||||
@admin.register(LoyaltyTransaction)
|
|
||||||
class LoyaltyTransactionAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('customer', 'transaction_type', 'points', 'created_at')
|
|
||||||
list_filter = ('transaction_type', 'created_at')
|
|
||||||
search_fields = ('customer__name',)
|
|
||||||
|
|
||||||
@admin.register(Device)
|
|
||||||
class DeviceAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'device_type', 'connection_type', 'ip_address', 'is_active')
|
|
||||||
list_filter = ('device_type', 'connection_type', 'is_active')
|
|
||||||
search_fields = ('name', 'ip_address')
|
|
||||||
|
|
||||||
@admin.register(CashierCounterRegistry)
|
|
||||||
class CashierCounterRegistryAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('cashier', 'counter', 'assigned_at')
|
|
||||||
search_fields = ('cashier__username', 'cashier__first_name', 'counter__name')
|
|
||||||
|
|||||||
@ -1,40 +1,13 @@
|
|||||||
from .models import SystemSetting
|
|
||||||
from django.db.utils import OperationalError
|
|
||||||
from django.core.management import call_command
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
STARTUP_TIMESTAMP = int(time.time())
|
|
||||||
|
|
||||||
def project_context(request):
|
def project_context(request):
|
||||||
|
"""
|
||||||
|
Adds project-specific environment variables to the template context globally.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
"deployment_timestamp": STARTUP_TIMESTAMP,
|
# Used for cache-busting static assets
|
||||||
}
|
"deployment_timestamp": int(time.time()),
|
||||||
|
|
||||||
def global_settings(request):
|
|
||||||
settings = None
|
|
||||||
try:
|
|
||||||
# Use a quick query to avoid hangs if DB is locked
|
|
||||||
settings = SystemSetting.objects.first()
|
|
||||||
if not settings:
|
|
||||||
# Only attempt creation if we are absolutely sure it's missing
|
|
||||||
# and wrap it in a try-except to avoid crashes on read-only DBs
|
|
||||||
try:
|
|
||||||
settings = SystemSetting.objects.create(business_name="Meezan Accounting")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to create SystemSetting: {e}")
|
|
||||||
settings = None
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Database error in global_settings: {e}")
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
|
||||||
'site_settings': settings,
|
|
||||||
'global_settings': settings,
|
|
||||||
'decimal_places': settings.decimal_places if settings else 3
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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')
|
|
||||||
@ -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}")
|
|
||||||
@ -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>")
|
|
||||||
@ -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__'
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
|
|
||||||
class ImportFileForm(forms.Form):
|
|
||||||
file = forms.FileField(label="Excel File (.xlsx)")
|
|
||||||
@ -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>")
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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)'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||