Autosave: 20260202-093633

This commit is contained in:
Flatlogic Bot 2026-02-02 09:36:33 +00:00
parent 7d1c8df2b2
commit 5391ba1010
33 changed files with 3038 additions and 304 deletions

View File

@ -1,15 +1,19 @@
from django.contrib import admin from django.contrib import admin
from .models import Category, Product, Customer, Supplier, Sale, SaleItem, Purchase from .models import Category, Unit, Product, Customer, Supplier, Sale, SaleItem, Purchase, SystemSetting
@admin.register(Category) @admin.register(Category)
class CategoryAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin):
list_display = ('name_en', 'name_ar', 'slug') list_display = ('name_en', 'name_ar', 'slug')
prepopulated_fields = {'slug': ('name_en',)} prepopulated_fields = {'slug': ('name_en',)}
@admin.register(Unit)
class UnitAdmin(admin.ModelAdmin):
list_display = ('name_en', 'name_ar', 'short_name')
@admin.register(Product) @admin.register(Product)
class ProductAdmin(admin.ModelAdmin): class ProductAdmin(admin.ModelAdmin):
list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category') list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category', 'unit')
list_filter = ('category',) list_filter = ('category', 'unit')
search_fields = ('name_en', 'name_ar', 'sku') search_fields = ('name_en', 'name_ar', 'sku')
@admin.register(Customer) @admin.register(Customer)
@ -34,3 +38,7 @@ class SaleAdmin(admin.ModelAdmin):
class PurchaseAdmin(admin.ModelAdmin): class PurchaseAdmin(admin.ModelAdmin):
list_display = ('id', 'supplier', 'total_amount', 'created_at') list_display = ('id', 'supplier', 'total_amount', 'created_at')
list_filter = ('supplier', 'created_at') list_filter = ('supplier', 'created_at')
@admin.register(SystemSetting)
class SystemSettingAdmin(admin.ModelAdmin):
list_display = ('business_name', 'phone', 'email', 'vat_number')

View File

@ -0,0 +1,67 @@
# 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 = [
migrations.RemoveField(
model_name='systemsetting',
name='logo_url',
),
migrations.AddField(
model_name='systemsetting',
name='logo',
field=models.ImageField(blank=True, null=True, upload_to='business_logos/', verbose_name='Logo'),
),
migrations.AddField(
model_name='systemsetting',
name='registration_number',
field=models.CharField(blank=True, max_length=50, verbose_name='Registration Number'),
),
migrations.AddField(
model_name='systemsetting',
name='vat_number',
field=models.CharField(blank=True, max_length=50, verbose_name='VAT Number'),
),
migrations.AlterField(
model_name='product',
name='price',
field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Price'),
),
migrations.AlterField(
model_name='purchase',
name='total_amount',
field=models.DecimalField(decimal_places=3, max_digits=15),
),
migrations.AlterField(
model_name='sale',
name='discount',
field=models.DecimalField(decimal_places=3, default=0, max_digits=15),
),
migrations.AlterField(
model_name='sale',
name='total_amount',
field=models.DecimalField(decimal_places=3, max_digits=15),
),
migrations.AlterField(
model_name='saleitem',
name='line_total',
field=models.DecimalField(decimal_places=3, max_digits=15),
),
migrations.AlterField(
model_name='saleitem',
name='unit_price',
field=models.DecimalField(decimal_places=3, max_digits=12),
),
migrations.AlterField(
model_name='systemsetting',
name='currency_symbol',
field=models.CharField(default='OMR', max_length=10, verbose_name='Currency Symbol'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-02 08:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_remove_systemsetting_logo_url_systemsetting_logo_and_more'),
]
operations = [
migrations.CreateModel(
name='Unit',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name_en', models.CharField(max_length=50, verbose_name='Name (English)')),
('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')),
('short_name', models.CharField(max_length=10, verbose_name='Short Name')),
],
),
migrations.AddField(
model_name='product',
name='unit',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.unit'),
),
]

View File

@ -0,0 +1,59 @@
# Generated by Django 5.2.7 on 2026-02-02 08:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_unit_product_unit'),
]
operations = [
migrations.AddField(
model_name='product',
name='cost_price',
field=models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='Cost Price'),
),
migrations.AddField(
model_name='product',
name='is_active',
field=models.BooleanField(default=True, verbose_name='Active'),
),
migrations.AddField(
model_name='product',
name='opening_stock',
field=models.PositiveIntegerField(default=0, verbose_name='Opening Stock'),
),
migrations.AddField(
model_name='product',
name='supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.supplier'),
),
migrations.AddField(
model_name='product',
name='vat',
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='VAT (%)'),
),
migrations.AlterField(
model_name='product',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='product_images/', verbose_name='Product Image'),
),
migrations.AlterField(
model_name='product',
name='price',
field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Sale Price'),
),
migrations.AlterField(
model_name='product',
name='sku',
field=models.CharField(max_length=50, unique=True, verbose_name='Barcode/SKU'),
),
migrations.AlterField(
model_name='product',
name='stock_quantity',
field=models.PositiveIntegerField(default=0, verbose_name='In Stock'),
),
]

View File

@ -0,0 +1,82 @@
# Generated by Django 5.2.7 on 2026-02-02 08:35
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_product_cost_price_product_is_active_and_more'),
]
operations = [
migrations.AddField(
model_name='purchase',
name='balance_due',
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Balance Due'),
),
migrations.AddField(
model_name='purchase',
name='due_date',
field=models.DateField(blank=True, null=True, verbose_name='Due Date'),
),
migrations.AddField(
model_name='purchase',
name='invoice_number',
field=models.CharField(blank=True, max_length=50, verbose_name='Invoice Number'),
),
migrations.AddField(
model_name='purchase',
name='notes',
field=models.TextField(blank=True, verbose_name='Notes'),
),
migrations.AddField(
model_name='purchase',
name='paid_amount',
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Paid Amount'),
),
migrations.AddField(
model_name='purchase',
name='payment_type',
field=models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit'), ('partial', 'Partial')], default='cash', max_length=20, verbose_name='Payment Type'),
),
migrations.AddField(
model_name='purchase',
name='status',
field=models.CharField(choices=[('paid', 'Paid'), ('partial', 'Partial'), ('unpaid', 'Unpaid')], default='paid', max_length=20, verbose_name='Status'),
),
migrations.AlterField(
model_name='purchase',
name='supplier',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchases', to='core.supplier'),
),
migrations.AlterField(
model_name='purchase',
name='total_amount',
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount'),
),
migrations.CreateModel(
name='PurchaseItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
('cost_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Cost Price')),
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
('purchase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.purchase')),
],
),
migrations.CreateModel(
name='PurchasePayment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')),
('payment_date', models.DateField(default=django.utils.timezone.now, verbose_name='Payment Date')),
('payment_method', models.CharField(default='Cash', max_length=50, verbose_name='Payment Method')),
('notes', models.TextField(blank=True, verbose_name='Notes')),
('purchase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='core.purchase')),
],
),
]

View File

@ -0,0 +1,91 @@
# Generated by Django 5.2.7 on 2026-02-02 09:25
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_purchase_balance_due_purchase_due_date_and_more'),
]
operations = [
migrations.AddField(
model_name='sale',
name='balance_due',
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Balance Due'),
),
migrations.AddField(
model_name='sale',
name='due_date',
field=models.DateField(blank=True, null=True, verbose_name='Due Date'),
),
migrations.AddField(
model_name='sale',
name='invoice_number',
field=models.CharField(blank=True, max_length=50, verbose_name='Invoice Number'),
),
migrations.AddField(
model_name='sale',
name='notes',
field=models.TextField(blank=True, verbose_name='Notes'),
),
migrations.AddField(
model_name='sale',
name='paid_amount',
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Paid Amount'),
),
migrations.AddField(
model_name='sale',
name='payment_type',
field=models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit'), ('partial', 'Partial')], default='cash', max_length=20, verbose_name='Payment Type'),
),
migrations.AddField(
model_name='sale',
name='status',
field=models.CharField(choices=[('paid', 'Paid'), ('partial', 'Partial'), ('unpaid', 'Unpaid')], default='paid', max_length=20, verbose_name='Status'),
),
migrations.AlterField(
model_name='sale',
name='customer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to='core.customer'),
),
migrations.AlterField(
model_name='sale',
name='discount',
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Discount'),
),
migrations.AlterField(
model_name='sale',
name='total_amount',
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount'),
),
migrations.AlterField(
model_name='saleitem',
name='line_total',
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total'),
),
migrations.AlterField(
model_name='saleitem',
name='quantity',
field=models.PositiveIntegerField(verbose_name='Quantity'),
),
migrations.AlterField(
model_name='saleitem',
name='unit_price',
field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price'),
),
migrations.CreateModel(
name='SalePayment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')),
('payment_date', models.DateField(default=django.utils.timezone.now, verbose_name='Payment Date')),
('payment_method', models.CharField(default='Cash', max_length=50, verbose_name='Payment Method')),
('notes', models.TextField(blank=True, verbose_name='Notes')),
('sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='core.sale')),
],
),
]

View File

@ -1,5 +1,6 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone
class Category(models.Model): class Category(models.Model):
name_en = models.CharField(_("Name (English)"), max_length=100) name_en = models.CharField(_("Name (English)"), max_length=100)
@ -12,15 +13,29 @@ class Category(models.Model):
def __str__(self): def __str__(self):
return f"{self.name_en} / {self.name_ar}" return f"{self.name_en} / {self.name_ar}"
class Unit(models.Model):
name_en = models.CharField(_("Name (English)"), max_length=50)
name_ar = models.CharField(_("Name (Arabic)"), max_length=50)
short_name = models.CharField(_("Short Name"), max_length=10)
def __str__(self):
return f"{self.name_en} / {self.name_ar}"
class Product(models.Model): class Product(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products") category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products")
unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, related_name="products")
supplier = models.ForeignKey('Supplier', on_delete=models.SET_NULL, null=True, blank=True, related_name="products")
name_en = models.CharField(_("Name (English)"), max_length=200) name_en = models.CharField(_("Name (English)"), max_length=200)
name_ar = models.CharField(_("Name (Arabic)"), max_length=200) name_ar = models.CharField(_("Name (Arabic)"), max_length=200)
sku = models.CharField(_("SKU"), max_length=50, unique=True) sku = models.CharField(_("Barcode/SKU"), max_length=50, unique=True)
description = models.TextField(_("Description"), blank=True) description = models.TextField(_("Description"), blank=True)
price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2) cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3, default=0)
stock_quantity = models.PositiveIntegerField(_("Stock Quantity"), default=0) price = models.DecimalField(_("Sale Price"), max_digits=12, decimal_places=3)
image = models.URLField(_("Product Image"), blank=True, null=True) vat = models.DecimalField(_("VAT (%)"), max_digits=5, decimal_places=2, default=0)
opening_stock = models.PositiveIntegerField(_("Opening Stock"), default=0)
stock_quantity = models.PositiveIntegerField(_("In Stock"), default=0)
image = models.ImageField(_("Product Image"), upload_to="product_images/", blank=True, null=True)
is_active = models.BooleanField(_("Active"), default=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
@ -44,34 +59,132 @@ class Supplier(models.Model):
return self.name return self.name
class Sale(models.Model): class Sale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True) PAYMENT_TYPE_CHOICES = [
total_amount = models.DecimalField(max_digits=12, decimal_places=2) ('cash', _('Cash')),
discount = models.DecimalField(max_digits=12, decimal_places=2, default=0) ('credit', _('Credit')),
('partial', _('Partial')),
]
STATUS_CHOICES = [
('paid', _('Paid')),
('partial', _('Partial')),
('unpaid', _('Unpaid')),
]
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales")
invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True)
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0)
balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0)
discount = models.DecimalField(_("Discount"), max_digits=15, decimal_places=3, default=0)
payment_type = models.CharField(_("Payment Type"), max_length=20, choices=PAYMENT_TYPE_CHOICES, default='cash')
status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid')
due_date = models.DateField(_("Due Date"), null=True, blank=True)
notes = models.TextField(_("Notes"), blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return f"Sale #{self.id} - {self.total_amount}" return f"Sale #{self.id} - {self.customer.name if self.customer else 'Guest'}"
def update_balance(self):
payments_total = self.payments.aggregate(total=models.Sum('amount'))['total'] or 0
self.paid_amount = payments_total
self.balance_due = self.total_amount - self.paid_amount
if self.balance_due <= 0:
self.status = 'paid'
elif self.paid_amount > 0:
self.status = 'partial'
else:
self.status = 'unpaid'
self.save()
class SaleItem(models.Model): class SaleItem(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="items") sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="items")
product = models.ForeignKey(Product, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField() quantity = models.PositiveIntegerField(_("Quantity"))
unit_price = models.DecimalField(max_digits=10, decimal_places=2) unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3)
line_total = models.DecimalField(max_digits=12, decimal_places=2) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3)
def __str__(self):
return f"{self.product.name_en} x {self.quantity}"
class SalePayment(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="payments")
amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3)
payment_date = models.DateField(_("Payment Date"), default=timezone.now)
payment_method = models.CharField(_("Payment Method"), max_length=50, default="Cash")
notes = models.TextField(_("Notes"), blank=True)
def __str__(self):
return f"Payment of {self.amount} for Sale #{self.sale.id}"
class Purchase(models.Model): class Purchase(models.Model):
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True) PAYMENT_TYPE_CHOICES = [
total_amount = models.DecimalField(max_digits=12, decimal_places=2) ('cash', _('Cash')),
('credit', _('Credit')),
('partial', _('Partial')),
]
STATUS_CHOICES = [
('paid', _('Paid')),
('partial', _('Partial')),
('unpaid', _('Unpaid')),
]
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, related_name="purchases")
invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True)
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0)
balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0)
payment_type = models.CharField(_("Payment Type"), max_length=20, choices=PAYMENT_TYPE_CHOICES, default='cash')
status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid')
due_date = models.DateField(_("Due Date"), null=True, blank=True)
notes = models.TextField(_("Notes"), blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Purchase #{self.id} - {self.supplier.name if self.supplier else 'N/A'}"
def update_balance(self):
payments_total = self.payments.aggregate(total=models.Sum('amount'))['total'] or 0
self.paid_amount = payments_total
self.balance_due = self.total_amount - self.paid_amount
if self.balance_due <= 0:
self.status = 'paid'
elif self.paid_amount > 0:
self.status = 'partial'
else:
self.status = 'unpaid'
self.save()
class PurchaseItem(models.Model):
purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="items")
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(_("Quantity"))
cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3)
line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3)
def __str__(self):
return f"{self.product.name_en} x {self.quantity}"
class PurchasePayment(models.Model):
purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="payments")
amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3)
payment_date = models.DateField(_("Payment Date"), default=timezone.now)
payment_method = models.CharField(_("Payment Method"), max_length=50, default="Cash")
notes = models.TextField(_("Notes"), blank=True)
def __str__(self):
return f"Payment of {self.amount} for Purchase #{self.purchase.id}"
class SystemSetting(models.Model): class SystemSetting(models.Model):
business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting") business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting")
address = models.TextField(_("Address"), blank=True) address = models.TextField(_("Address"), blank=True)
phone = models.CharField(_("Phone"), max_length=20, blank=True) phone = models.CharField(_("Phone"), max_length=20, blank=True)
email = models.EmailField(_("Email"), blank=True) email = models.EmailField(_("Email"), blank=True)
currency_symbol = models.CharField(_("Currency Symbol"), max_length=10, default="$") currency_symbol = models.CharField(_("Currency Symbol"), max_length=10, default="OMR")
tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0) tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0)
logo_url = models.URLField(_("Logo URL"), blank=True, null=True) logo = models.ImageField(_("Logo"), upload_to="business_logos/", blank=True, null=True)
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True)
def __str__(self): def __str__(self):
return self.business_name return self.business_name

View File

@ -32,8 +32,8 @@
<nav id="sidebar"> <nav id="sidebar">
<div class="sidebar-header d-flex align-items-center"> <div class="sidebar-header d-flex align-items-center">
<a class="navbar-brand fw-bold text-primary fs-4" href="{% url 'index' %}"> <a class="navbar-brand fw-bold text-primary fs-4" href="{% url 'index' %}">
{% if site_settings.logo_url %} {% if site_settings.logo %}
<img src="{{ site_settings.logo_url }}" alt="Logo" height="30" class="me-2"> <img src="{{ site_settings.logo.url }}" alt="Logo" height="30" class="me-2">
{% else %} {% else %}
<i class="bi bi-calculator-fill me-2"></i> <i class="bi bi-calculator-fill me-2"></i>
{% endif %} {% endif %}
@ -52,6 +52,11 @@
<i class="bi bi-shop"></i> {% trans "POS System" %} <i class="bi bi-shop"></i> {% trans "POS System" %}
</a> </a>
</li> </li>
<li>
<a href="{% url 'invoices' %}" class="{% if request.resolver_match.url_name == 'invoices' %}active{% endif %}">
<i class="bi bi-file-earmark-text"></i> {% trans "Invoices" %}
</a>
</li>
<li> <li>
<a href="{% url 'reports' %}" class="{% if request.resolver_match.url_name == 'reports' %}active{% endif %}"> <a href="{% url 'reports' %}" class="{% if request.resolver_match.url_name == 'reports' %}active{% endif %}">
<i class="bi bi-graph-up-arrow"></i> {% trans "Reports" %} <i class="bi bi-graph-up-arrow"></i> {% trans "Reports" %}

View File

@ -1,13 +1,21 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Customers" %} | Meezan Accounting{% endblock %} {% block title %}{% trans "Customers" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">{% trans "Customers" %}</h2> <div>
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addCustomerModal"> <h2 class="fw-bold mb-1">{% trans "Customers" %}</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'index' %}">{% trans "Dashboard" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Customers" %}</li>
</ol>
</nav>
</div>
<button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
<i class="bi bi-person-plus me-2"></i>{% trans "Add Customer" %} <i class="bi bi-person-plus me-2"></i>{% trans "Add Customer" %}
</button> </button>
</div> </div>
@ -17,7 +25,7 @@
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -32,7 +40,6 @@
<th class="ps-4">{% trans "Name" %}</th> <th class="ps-4">{% trans "Name" %}</th>
<th>{% trans "Phone" %}</th> <th>{% trans "Phone" %}</th>
<th>{% trans "Email" %}</th> <th>{% trans "Email" %}</th>
<th>{% trans "Address" %}</th>
<th>{% trans "Total Sales" %}</th> <th>{% trans "Total Sales" %}</th>
<th class="text-end pe-4">{% trans "Actions" %}</th> <th class="text-end pe-4">{% trans "Actions" %}</th>
</tr> </tr>
@ -40,20 +47,105 @@
<tbody> <tbody>
{% for customer in customers %} {% for customer in customers %}
<tr> <tr>
<td class="ps-4 fw-bold">{{ customer.name }}</td> <td class="ps-4 fw-bold text-dark">{{ customer.name }}</td>
<td>{{ customer.phone }}</td> <td>{{ customer.phone|default:"-" }}</td>
<td>{{ customer.email }}</td> <td>{{ customer.email|default:"-" }}</td>
<td>{{ customer.address|truncatechars:30 }}</td> <td><span class="badge bg-primary bg-opacity-10 text-primary">{{ site_settings.currency_symbol }}{{ customer.total_sales|default:"0.000"|floatformat:3 }}</span></td>
<td><span class="badge bg-primary bg-opacity-10 text-primary">{{ site_settings.currency_symbol }}{{ customer.total_sales|default:"0.00" }}</span></td>
<td class="text-end pe-4"> <td class="text-end pe-4">
<button class="btn btn-sm btn-light rounded-circle"><i class="bi bi-pencil"></i></button> <div class="btn-group">
<button class="btn btn-sm btn-light rounded-circle text-danger"><i class="bi bi-trash"></i></button> <button class="btn btn-sm btn-light rounded-pill px-2 me-1" data-bs-toggle="modal" data-bs-target="#viewCustomerModal{{ customer.id }}" title="{% trans 'View' %}">
<i class="bi bi-eye text-primary"></i>
</button>
<button class="btn btn-sm btn-light rounded-pill px-2 me-1" data-bs-toggle="modal" data-bs-target="#editCustomerModal{{ customer.id }}" title="{% trans 'Edit' %}">
<i class="bi bi-pencil text-success"></i>
</button>
<a href="{% url 'delete_customer' customer.id %}" class="btn btn-sm btn-light rounded-pill px-2" onclick="return confirm('{% trans 'Are you sure you want to delete this customer?' %}')" title="{% trans 'Delete' %}">
<i class="bi bi-trash text-danger"></i>
</a>
</div>
</td> </td>
</tr> </tr>
<!-- View Customer Modal -->
<div class="modal fade" id="viewCustomerModal{{ customer.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Customer Details" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-center mb-4">
<div class="bg-primary bg-opacity-10 text-primary rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
<i class="bi bi-person" style="font-size: 2.5rem;"></i>
</div>
<h4 class="fw-bold mb-0">{{ customer.name }}</h4>
</div>
<hr>
<div class="row g-3">
<div class="col-6">
<small class="text-muted d-block">{% trans "Phone" %}</small>
<span class="fw-bold">{{ customer.phone|default:"-" }}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Email" %}</small>
<span class="fw-bold">{{ customer.email|default:"-" }}</span>
</div>
<div class="col-12">
<small class="text-muted d-block">{% trans "Address" %}</small>
<span class="fw-bold">{{ customer.address|default:"-" }}</span>
</div>
<div class="col-12">
<small class="text-muted d-block">{% trans "Total Sales" %}</small>
<span class="badge bg-primary bg-opacity-10 text-primary fs-6">{{ site_settings.currency_symbol }}{{ customer.total_sales|default:"0.000"|floatformat:3 }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Customer Modal -->
<div class="modal fade" id="editCustomerModal{{ customer.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Edit Customer" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'edit_customer' customer.id %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Full Name" %}</label>
<input type="text" name="name" class="form-control rounded-3" value="{{ customer.name }}" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Phone Number" %}</label>
<input type="text" name="phone" class="form-control rounded-3" value="{{ customer.phone }}">
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Email Address" %}</label>
<input type="email" name="email" class="form-control rounded-3" value="{{ customer.email }}">
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Address" %}</label>
<textarea name="address" class="form-control rounded-3" rows="3">{{ customer.address }}</textarea>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Update Customer" %}</button>
</div>
</form>
</div>
</div>
</div>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="6" class="text-center py-5 text-muted"> <td colspan="5" class="text-center py-5">
{% trans "No customers found." %} <i class="bi bi-person text-muted opacity-25" style="font-size: 3rem;"></i>
<p class="mt-2 text-muted">{% trans "No customers found." %}</p>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -65,12 +157,12 @@
</div> </div>
<!-- Add Customer Modal --> <!-- Add Customer Modal -->
<div class="modal fade" id="addCustomerModal" tabindex="-1"> <div class="modal fade" id="addCustomerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4"> <div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0"> <div class="modal-header border-0 pb-0">
<h5 class="fw-bold">{% trans "Add New Customer" %}</h5> <h5 class="modal-title fw-bold">{% trans "Add New Customer" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form action="{% url 'add_customer' %}" method="POST"> <form action="{% url 'add_customer' %}" method="POST">
{% csrf_token %} {% csrf_token %}

View File

@ -28,7 +28,7 @@
</div> </div>
<div> <div>
<h6 class="text-muted small mb-1">{% trans "Total Revenue" %}</h6> <h6 class="text-muted small mb-1">{% trans "Total Revenue" %}</h6>
<h4 class="fw-bold mb-0">{{ site_settings.currency_symbol }}{{ total_sales_amount|floatformat:2 }}</h4> <h4 class="fw-bold mb-0">{{ site_settings.currency_symbol }}{{ total_sales_amount|floatformat:3 }}</h4>
</div> </div>
</div> </div>
</div> </div>
@ -147,7 +147,7 @@
<td class="fw-bold">#{{ sale.id }}</td> <td class="fw-bold">#{{ sale.id }}</td>
<td>{{ sale.customer.name|default:"Guest" }}</td> <td>{{ sale.customer.name|default:"Guest" }}</td>
<td>{{ sale.created_at|date:"M d, Y H:i" }}</td> <td>{{ sale.created_at|date:"M d, Y H:i" }}</td>
<td class="fw-bold">{{ site_settings.currency_symbol }}{{ sale.total_amount }}</td> <td class="fw-bold">{{ site_settings.currency_symbol }}{{ sale.total_amount|floatformat:3 }}</td>
<td><span class="badge bg-success bg-opacity-10 text-success">{% trans "Completed" %}</span></td> <td><span class="badge bg-success bg-opacity-10 text-success">{% trans "Completed" %}</span></td>
<td> <td>
<button class="btn btn-sm btn-light"> <button class="btn btn-sm btn-light">
@ -217,4 +217,4 @@
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,23 +1,31 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static %} {% load i18n static %}
{% block title %}{% trans "Inventory Management" %} | Meezan Accounting{% endblock %} {% block title %}{% trans "Stock Management" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h2 class="fw-bold mb-1">{% trans "Inventory Management" %}</h2> <h2 class="fw-bold mb-1">{% trans "Stock Management" %}</h2>
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'index' %}">{% trans "Dashboard" %}</a></li> <li class="breadcrumb-item"><a href="{% url 'index' %}">{% trans "Dashboard" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Inventory" %}</li> <li class="breadcrumb-item active" aria-current="page">{% trans "Stock" %}</li>
</ol> </ol>
</nav> </nav>
</div> </div>
<button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addProductModal"> <div class="btn-group">
<i class="bi bi-plus-lg me-2"></i>{% trans "Add Product" %} <button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addProductModal">
</button> <i class="bi bi-plus-lg me-2"></i>{% trans "Add Item" %}
</button>
<button class="btn btn-outline-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
<i class="bi bi-folder-plus me-2"></i>{% trans "Add Category" %}
</button>
<button class="btn btn-outline-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addUnitModal">
<i class="bi bi-rulers me-2"></i>{% trans "Add Unit" %}
</button>
</div>
</div> </div>
{% if messages %} {% if messages %}
@ -25,98 +33,479 @@
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<!-- Search & Filter Bar --> <!-- Tabs for Stock Management -->
<div class="card border-0 shadow-sm rounded-4 mb-4"> <ul class="nav nav-pills mb-4 bg-white p-2 rounded-4 shadow-sm" id="stockTabs" role="tablist">
<div class="card-body p-3"> <li class="nav-item" role="presentation">
<form class="row g-3"> <button class="nav-link active px-4 rounded-3" id="items-tab" data-bs-toggle="tab" data-bs-target="#items" type="button" role="tab">
<div class="col-md-6"> <i class="bi bi-box-seam me-2"></i>{% trans "Items" %}
<div class="input-group"> </button>
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span> </li>
<input type="text" class="form-control border-start-0" placeholder="{% trans 'Search by name or SKU...' %}"> <li class="nav-item" role="presentation">
</div> <button class="nav-link px-4 rounded-3" id="categories-tab" data-bs-toggle="tab" data-bs-target="#categories-list" type="button" role="tab">
</div> <i class="bi bi-folder me-2"></i>{% trans "Categories" %}
<div class="col-md-4"> </button>
<select class="form-select"> </li>
<option value="">{% trans "All Categories" %}</option> <li class="nav-item" role="presentation">
{% for category in categories %} <button class="nav-link px-4 rounded-3" id="units-tab" data-bs-toggle="tab" data-bs-target="#units-list" type="button" role="tab">
<option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option> <i class="bi bi-rulers me-2"></i>{% trans "Units" %}
{% endfor %} </button>
</select> </li>
</div> </ul>
<div class="col-md-2">
<button type="button" class="btn btn-light w-100">{% trans "Filter" %}</button>
</div>
</form>
</div>
</div>
<!-- Product Grid --> <div class="tab-content" id="stockTabsContent">
<div class="row g-4"> <!-- Items Tab -->
{% for product in products %} <div class="tab-pane fade show active" id="items" role="tabpanel">
<div class="col-md-6 col-lg-4 col-xl-3"> <!-- Search & Filter Bar -->
<div class="card product-card h-100 shadow-sm border-0"> <div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="position-relative"> <div class="card-body p-3">
{% if product.image %} <form class="row g-3">
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name_en }}" style="height: 200px; object-fit: cover;"> <div class="col-md-6">
{% else %} <div class="input-group">
<div class="bg-secondary bg-opacity-10 d-flex align-items-center justify-content-center" style="height: 200px;"> <span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
<i class="bi bi-box-seam text-secondary opacity-25" style="font-size: 4rem;"></i> <input type="text" class="form-control border-start-0" placeholder="{% trans 'Search by name or SKU...' %}">
</div> </div>
{% endif %}
<span class="position-absolute top-0 end-0 m-3 badge {% if product.stock_quantity < 5 %}bg-danger{% else %}bg-success{% endif %}">
{% trans "In Stock" %}: {{ product.stock_quantity }}
</span>
</div>
<div class="card-body">
<div class="text-muted small mb-1">
{% if LANGUAGE_CODE == 'ar' %}{{ product.category.name_ar }}{% else %}{{ product.category.name_en }}{% endif %}
</div>
<h5 class="card-title fw-bold mb-2">
{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}
</h5>
<div class="d-flex justify-content-between align-items-center mt-3">
<h4 class="text-primary fw-bold mb-0">{{ site_settings.currency_symbol }}{{ product.price }}</h4>
<div class="btn-group">
<button class="btn btn-sm btn-outline-light text-dark"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-light text-dark"><i class="bi bi-eye"></i></button>
</div> </div>
</div> <div class="col-md-4">
<select class="form-select">
<option value="">{% trans "All Categories" %}</option>
{% for category in categories %}
<option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<button type="button" class="btn btn-light w-100">{% trans "Filter" %}</button>
</div>
</form>
</div>
</div>
<!-- Product Table (Grid) -->
<div class="card border-0 shadow-sm rounded-4">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">{% trans "Item" %}</th>
<th>{% trans "SKU/Barcode" %}</th>
<th>{% trans "Category" %}</th>
<th>{% trans "Stock" %}</th>
<th>{% trans "Cost" %}</th>
<th>{% trans "Price" %}</th>
<th>{% trans "Status" %}</th>
<th class="text-end pe-4">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr class="{% if not product.is_active %}opacity-50{% endif %}">
<td class="ps-4">
<div class="d-flex align-items-center">
{% if product.image %}
<img src="{{ product.image.url }}" class="rounded-2 me-3" alt="" style="width: 40px; height: 40px; object-fit: cover;">
{% else %}
<div class="bg-light rounded-2 me-3 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<i class="bi bi-box-seam text-muted"></i>
</div>
{% endif %}
<div>
<div class="fw-bold text-dark">{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}</div>
<small class="text-muted">{{ product.unit.name_en|default:"" }}</small>
</div>
</div>
</td>
<td><code>{{ product.sku }}</code></td>
<td>{% if LANGUAGE_CODE == 'ar' %}{{ product.category.name_ar }}{% else %}{{ product.category.name_en }}{% endif %}</td>
<td>
<span class="badge {% if product.stock_quantity < 5 %}bg-danger{% else %}bg-success-subtle text-success{% endif %} rounded-pill">
{{ product.stock_quantity }} {{ product.unit.short_name|default:"" }}
</span>
</td>
<td>{{ site_settings.currency_symbol }}{{ product.cost_price|floatformat:3 }}</td>
<td class="fw-bold text-primary">{{ site_settings.currency_symbol }}{{ product.price|floatformat:3 }}</td>
<td>
{% if product.is_active %}
<span class="badge bg-success-subtle text-success border-success-subtle">{% trans "Active" %}</span>
{% else %}
<span class="badge bg-secondary-subtle text-secondary">{% trans "Inactive" %}</span>
{% endif %}
</td>
<td class="text-end pe-4">
<div class="btn-group">
<button class="btn btn-sm btn-light rounded-pill px-2 me-1" data-bs-toggle="modal" data-bs-target="#viewProductModal{{ product.id }}" title="{% trans 'View' %}">
<i class="bi bi-eye text-primary"></i>
</button>
<button class="btn btn-sm btn-light rounded-pill px-2 me-1" data-bs-toggle="modal" data-bs-target="#editProductModal{{ product.id }}" title="{% trans 'Edit' %}">
<i class="bi bi-pencil text-success"></i>
</button>
<a href="{% url 'delete_product' product.id %}" class="btn btn-sm btn-light rounded-pill px-2" onclick="return confirm('{% trans 'Are you sure you want to delete this item?' %}')" title="{% trans 'Delete' %}">
<i class="bi bi-trash text-danger"></i>
</a>
</div>
</td>
</tr>
<!-- View Product Modal -->
<div class="modal fade" id="viewProductModal{{ product.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Item Details" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-4 text-center mb-3">
{% if product.image %}
<img src="{{ product.image.url }}" class="img-fluid rounded-4 shadow-sm" alt="">
{% else %}
<div class="bg-light rounded-4 d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="bi bi-box-seam text-muted opacity-25" style="font-size: 4rem;"></i>
</div>
{% endif %}
</div>
<div class="col-md-8">
<h3 class="fw-bold mb-1">{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}</h3>
<p class="text-muted mb-3">{{ product.sku }} | {% if LANGUAGE_CODE == 'ar' %}{{ product.category.name_ar }}{% else %}{{ product.category.name_en }}{% endif %}</p>
<hr>
<div class="row g-3">
<div class="col-6">
<small class="text-muted d-block">{% trans "Sale Price" %}</small>
<span class="h5 fw-bold text-primary">{{ site_settings.currency_symbol }}{{ product.price|floatformat:3 }}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Cost Price" %}</small>
<span class="h5 fw-bold">{{ site_settings.currency_symbol }}{{ product.cost_price|floatformat:3 }}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Stock Quantity" %}</small>
<span class="badge {% if product.stock_quantity < 5 %}bg-danger{% else %}bg-success{% endif %}">{{ product.stock_quantity }} {{ product.unit.short_name|default:"" }}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "VAT" %}</small>
<span>{{ product.vat }}%</span>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Supplier" %}</small>
<span>{{ product.supplier.name|default:"-" }}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Status" %}</small>
{% if product.is_active %}<span class="text-success">{% trans "Active" %}</span>{% else %}<span class="text-danger">{% trans "Inactive" %}</span>{% endif %}
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Entry Date" %}</small>
<span>{{ product.created_at|date:"Y-m-d H:i" }}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Opening Stock" %}</small>
<span>{{ product.opening_stock }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Product Modal -->
<div class="modal fade" id="editProductModal{{ product.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Edit Item" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'edit_product' product.id %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
<div class="row g-3">
<!-- Identity Section -->
<div class="col-12"><h6 class="fw-bold text-primary mb-0 mt-2">{% trans "Basic Information" %}</h6><hr class="my-2"></div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" name="name_en" class="form-control rounded-3" value="{{ product.name_en }}" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" name="name_ar" class="form-control rounded-3" value="{{ product.name_ar }}" dir="rtl" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Barcode / SKU" %}</label>
<input type="text" name="sku" class="form-control rounded-3" value="{{ product.sku }}" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Category" %}</label>
<select name="category" class="form-select rounded-3" required>
{% for category in categories %}
<option value="{{ category.id }}" {% if product.category.id == category.id %}selected{% endif %}>
{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Unit" %}</label>
<select name="unit" class="form-select rounded-3">
<option value="">{% trans "Select Unit" %}</option>
{% for unit in units %}
<option value="{{ unit.id }}" {% if product.unit.id == unit.id %}selected{% endif %}>
{% if LANGUAGE_CODE == 'ar' %}{{ unit.name_ar }}{% else %}{{ unit.name_en }}{% endif %}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
<select name="supplier" class="form-select rounded-3">
<option value="">{% trans "Select Supplier" %}</option>
{% for supplier in suppliers %}
<option value="{{ supplier.id }}" {% if product.supplier.id == supplier.id %}selected{% endif %}>
{{ supplier.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Change Picture" %}</label>
<input type="file" name="image" class="form-control rounded-3" accept="image/*">
{% if product.image %}
<small class="text-muted">{% trans "Current" %}: {{ product.image.name }}</small>
{% endif %}
</div>
<!-- Pricing & Stock Section -->
<div class="col-12"><h6 class="fw-bold text-primary mb-0 mt-3">{% trans "Pricing & Stock" %}</h6><hr class="my-2"></div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Cost Price" %}</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">{{ site_settings.currency_symbol }}</span>
<input type="number" step="0.001" name="cost_price" class="form-control rounded-3 border-start-0" value="{{ product.cost_price|floatformat:3 }}" required>
</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Sale Price" %}</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">{{ site_settings.currency_symbol }}</span>
<input type="number" step="0.001" name="price" class="form-control rounded-3 border-start-0" value="{{ product.price|floatformat:3 }}" required>
</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "VAT (%)" %}</label>
<div class="input-group">
<input type="number" step="0.01" name="vat" class="form-control rounded-3" value="{{ product.vat|floatformat:2 }}" required>
<span class="input-group-text bg-light">%</span>
</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Opening Stock" %}</label>
<input type="number" name="opening_stock" class="form-control rounded-3" value="{{ product.opening_stock }}" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "In Stock (Current)" %}</label>
<input type="number" name="stock_quantity" class="form-control rounded-3" value="{{ product.stock_quantity }}" required>
</div>
<div class="col-md-4 d-flex align-items-center pt-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="editActiveSwitch{{ product.id }}" {% if product.is_active %}checked{% endif %}>
<label class="form-check-label fw-bold small" for="editActiveSwitch{{ product.id }}">{% trans "Active / Visible" %}</label>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Update Product" %}</button>
</div>
</form>
</div>
</div>
</div>
{% empty %}
<tr>
<td colspan="8" class="text-center py-5">
<i class="bi bi-box-seam text-muted opacity-25" style="font-size: 3rem;"></i>
<p class="mt-2 text-muted">{% trans "No products found." %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
{% empty %}
<div class="col-12 text-center py-5"> <!-- Categories Tab -->
<div class="bg-white rounded-4 p-5 shadow-sm"> <div class="tab-pane fade" id="categories-list" role="tabpanel">
<i class="bi bi-box-seam text-muted opacity-25" style="font-size: 5rem;"></i> <div class="card border-0 shadow-sm rounded-4">
<h4 class="mt-3 text-muted">{% trans "Your inventory is empty" %}</h4> <div class="table-responsive">
<p class="text-muted">{% trans "Start by adding your first product to the system." %}</p> <table class="table table-hover align-middle mb-0">
<button class="btn btn-primary mt-3" data-bs-toggle="modal" data-bs-target="#addProductModal"> <thead class="bg-light">
{% trans "Add First Product" %} <tr>
</button> <th class="ps-4">{% trans "Category Name" %}</th>
<th>{% trans "Arabic Name" %}</th>
<th>{% trans "Slug" %}</th>
<th class="text-end pe-4">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td class="ps-4 fw-bold text-dark">{{ category.name_en }}</td>
<td dir="rtl">{{ category.name_ar }}</td>
<td><code>{{ category.slug }}</code></td>
<td class="text-end pe-4">
<div class="btn-group">
<button class="btn btn-sm btn-light rounded-pill px-2 me-1" data-bs-toggle="modal" data-bs-target="#editCategoryModal{{ category.id }}" title="{% trans 'Edit' %}">
<i class="bi bi-pencil text-success"></i>
</button>
<a href="{% url 'delete_category' category.id %}" class="btn btn-sm btn-light rounded-pill px-2" onclick="return confirm('{% trans 'Are you sure you want to delete this category?' %}')" title="{% trans 'Delete' %}">
<i class="bi bi-trash text-danger"></i>
</a>
</div>
</td>
</tr>
<!-- Edit Category Modal -->
<div class="modal fade" id="editCategoryModal{{ category.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Edit Category" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'edit_category' category.id %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" name="name_en" class="form-control rounded-3" value="{{ category.name_en }}" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" name="name_ar" class="form-control rounded-3" value="{{ category.name_ar }}" dir="rtl" required>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Update Category" %}</button>
</div>
</form>
</div>
</div>
</div>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">{% trans "No categories found." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Units Tab -->
<div class="tab-pane fade" id="units-list" role="tabpanel">
<div class="card border-0 shadow-sm rounded-4">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">{% trans "Unit Name" %}</th>
<th>{% trans "Arabic Name" %}</th>
<th>{% trans "Short Name" %}</th>
<th class="text-end pe-4">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for unit in units %}
<tr>
<td class="ps-4 fw-bold text-dark">{{ unit.name_en }}</td>
<td dir="rtl">{{ unit.name_ar }}</td>
<td><span class="badge bg-light text-dark border">{{ unit.short_name }}</span></td>
<td class="text-end pe-4">
<div class="btn-group">
<button class="btn btn-sm btn-light rounded-pill px-2 me-1" data-bs-toggle="modal" data-bs-target="#editUnitModal{{ unit.id }}" title="{% trans 'Edit' %}">
<i class="bi bi-pencil text-success"></i>
</button>
<a href="{% url 'delete_unit' unit.id %}" class="btn btn-sm btn-light rounded-pill px-2" onclick="return confirm('{% trans 'Are you sure you want to delete this unit?' %}')" title="{% trans 'Delete' %}">
<i class="bi bi-trash text-danger"></i>
</a>
</div>
</td>
</tr>
<!-- Edit Unit Modal -->
<div class="modal fade" id="editUnitModal{{ unit.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Edit Unit" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'edit_unit' unit.id %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" name="name_en" class="form-control rounded-3" value="{{ unit.name_en }}" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" name="name_ar" class="form-control rounded-3" value="{{ unit.name_ar }}" dir="rtl" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Short Name" %}</label>
<input type="text" name="short_name" class="form-control rounded-3" value="{{ unit.short_name }}" required>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Update Unit" %}</button>
</div>
</form>
</div>
</div>
</div>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">{% trans "No units found." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
<!-- Add Product Modal --> <!-- Add Product Modal -->
<div class="modal fade" id="addProductModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="addProductModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-xl">
<div class="modal-content rounded-4 border-0 shadow"> <div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0"> <div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Add New Product" %}</h5> <h5 class="modal-title fw-bold">{% trans "Add New Item" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form action="{% url 'add_product' %}" method="POST"> <form action="{% url 'add_product' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="modal-body"> <div class="modal-body">
<div class="row g-3"> <div class="row g-3">
<!-- Identity Section -->
<div class="col-12"><h6 class="fw-bold text-primary mb-0 mt-2">{% trans "Basic Information" %}</h6><hr class="my-2"></div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label> <label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" name="name_en" class="form-control rounded-3" required> <input type="text" name="name_en" class="form-control rounded-3" required>
@ -125,7 +514,11 @@
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label> <label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" name="name_ar" class="form-control rounded-3" dir="rtl" required> <input type="text" name="name_ar" class="form-control rounded-3" dir="rtl" required>
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Barcode / SKU" %}</label>
<input type="text" name="sku" class="form-control rounded-3" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Category" %}</label> <label class="form-label small fw-bold">{% trans "Category" %}</label>
<select name="category" class="form-select rounded-3" required> <select name="category" class="form-select rounded-3" required>
<option value="">{% trans "Select Category" %}</option> <option value="">{% trans "Select Category" %}</option>
@ -134,20 +527,65 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<label class="form-label small fw-bold">{% trans "SKU" %}</label> <label class="form-label small fw-bold">{% trans "Unit" %}</label>
<input type="text" name="sku" class="form-control rounded-3" required> <select name="unit" class="form-select rounded-3">
<option value="">{% trans "Select Unit" %}</option>
{% for unit in units %}
<option value="{{ unit.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ unit.name_ar }}{% else %}{{ unit.name_en }}{% endif %}</option>
{% endfor %}
</select>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Price" %}</label> <label class="form-label small fw-bold">{% trans "Supplier" %}</label>
<select name="supplier" class="form-select rounded-3">
<option value="">{% trans "Select Supplier" %}</option>
{% for supplier in suppliers %}
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Product Picture" %}</label>
<input type="file" name="image" class="form-control rounded-3" accept="image/*">
</div>
<!-- Pricing & Stock Section -->
<div class="col-12"><h6 class="fw-bold text-primary mb-0 mt-3">{% trans "Pricing & Stock" %}</h6><hr class="my-2"></div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Cost Price" %}</label>
<div class="input-group"> <div class="input-group">
<span class="input-group-text bg-light border-end-0">{{ site_settings.currency_symbol }}</span> <span class="input-group-text bg-light border-end-0">{{ site_settings.currency_symbol }}</span>
<input type="number" step="0.01" name="price" class="form-control rounded-3 border-start-0" required> <input type="number" step="0.001" name="cost_price" class="form-control rounded-3 border-start-0" value="0.000" required>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Initial Stock" %}</label> <label class="form-label small fw-bold">{% trans "Sale Price" %}</label>
<input type="number" name="stock" class="form-control rounded-3" value="0" required> <div class="input-group">
<span class="input-group-text bg-light border-end-0">{{ site_settings.currency_symbol }}</span>
<input type="number" step="0.001" name="price" class="form-control rounded-3 border-start-0" value="0.000" required>
</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "VAT (%)" %}</label>
<div class="input-group">
<input type="number" step="0.01" name="vat" class="form-control rounded-3" value="0.00" required>
<span class="input-group-text bg-light">%</span>
</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Opening Stock" %}</label>
<input type="number" name="opening_stock" class="form-control rounded-3" value="0" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "In Stock (Current)" %}</label>
<input type="number" name="stock_quantity" class="form-control rounded-3" value="0" required>
</div>
<div class="col-md-4 d-flex align-items-center pt-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="isActiveSwitch" checked>
<label class="form-check-label fw-bold small" for="isActiveSwitch">{% trans "Active / Visible" %}</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -159,4 +597,70 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Add Category Modal -->
<div class="modal fade" id="addCategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Add New Category" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'add_category' %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" name="name_en" class="form-control rounded-3" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" name="name_ar" class="form-control rounded-3" dir="rtl" required>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Category" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Add Unit Modal -->
<div class="modal fade" id="addUnitModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Add New Unit" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'add_unit' %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" name="name_en" class="form-control rounded-3" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" name="name_ar" class="form-control rounded-3" dir="rtl" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Short Name (e.g., kg, pcs)" %}</label>
<input type="text" name="short_name" class="form-control rounded-3" required>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Unit" %}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,287 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "New Invoice" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container-fluid px-4" id="saleApp">
<div class="row">
<!-- Main Form -->
<div class="col-lg-8">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-header bg-white border-0 pt-4 px-4">
<h5 class="fw-bold mb-0"><i class="bi bi-receipt me-2 text-primary"></i>{% trans "Create Sales Invoice" %}</h5>
</div>
<div class="card-body p-4">
<!-- Customer & Invoice Info -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Customer" %}</label>
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="customerId">
<option value="">{% trans "Walking Customer / Guest" %}</option>
{% for customer in customers %}
<option value="{{ customer.id }}">{{ customer.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Invoice #" %}</label>
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="invoiceNumber" placeholder="{% trans 'e.g. INV-1001' %}">
</div>
</div>
<!-- Item Selection -->
<div class="mb-4">
<label class="form-label small fw-bold">{% trans "Search Products" %}</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0 border-secondary-subtle"><i class="bi bi-search"></i></span>
<input type="text" class="form-control rounded-3 border-start-0 border-secondary-subtle shadow-none" placeholder="{% trans 'Search by Name or SKU...' %}" v-model="searchQuery" @input="filterProducts">
</div>
<div class="position-relative">
<div class="list-group position-absolute w-100 shadow rounded-3 mt-1" style="z-index: 1000;" v-if="filteredProducts.length > 0">
<button v-for="product in filteredProducts" :key="product.id" class="list-group-item list-group-item-action border-0 py-3" @click="addItem(product)">
<div class="d-flex justify-content-between">
<div>
<span class="fw-bold">[[ product.name_en ]]</span> / [[ product.name_ar ]]
<div class="text-muted small">SKU: [[ product.sku ]] | Stock: [[ product.stock ]]</div>
</div>
<div class="text-primary fw-bold">[[ currencySymbol ]][[ product.price ]]</div>
</div>
</button>
</div>
</div>
</div>
<!-- Items Table -->
<div class="table-responsive">
<table class="table align-middle">
<thead class="bg-light-subtle">
<tr class="small text-uppercase text-muted fw-bold">
<th style="width: 40%;">{% trans "Product" %}</th>
<th class="text-center">{% trans "Unit Price" %}</th>
<th class="text-center" style="width: 15%;">{% trans "Quantity" %}</th>
<th class="text-end">{% trans "Total" %}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in cart" :key="index">
<td>
<div class="fw-bold">[[ item.name_en ]]</div>
<div class="text-muted small">[[ item.sku ]]</div>
</td>
<td>
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
<td class="text-end">
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
</td>
</tr>
<tr v-if="cart.length === 0">
<td colspan="5" class="text-center py-5 text-muted">
{% trans "Search and add products to this invoice." %}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Invoice Summary -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm rounded-4 sticky-top" style="top: 20px;">
<div class="card-body p-4">
<h5 class="fw-bold mb-4">{% trans "Invoice Summary" %}</h5>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">{% trans "Subtotal" %}</span>
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">{% trans "Discount" %}</span>
<div class="input-group input-group-sm" style="width: 120px;">
<input type="number" class="form-control text-end border-0 border-bottom rounded-0" v-model="discount" @input="calculateTotal">
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between mb-4">
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(3) ]]</h4>
</div>
<!-- Payment Details -->
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Payment Method" %}</label>
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="paymentType">
<option value="cash">{% trans "Cash (Full)" %}</option>
<option value="credit">{% trans "Credit (Unpaid)" %}</option>
<option value="partial">{% trans "Partial Payment" %}</option>
</select>
</div>
<div class="mb-3" v-if="paymentType === 'partial'">
<label class="form-label small fw-bold">{% trans "Paid Amount" %}</label>
<input type="number" step="0.001" class="form-control rounded-3" v-model="paidAmount">
</div>
<div class="mb-4" v-if="paymentType !== 'cash'">
<label class="form-label small fw-bold">{% trans "Due Date" %}</label>
<input type="date" class="form-control rounded-3" v-model="dueDate">
</div>
<div class="mb-4">
<label class="form-label small fw-bold">{% trans "Internal Notes" %}</label>
<textarea class="form-control rounded-3" rows="2" v-model="notes"></textarea>
</div>
<div class="d-grid">
<button class="btn btn-primary rounded-3 py-3 fw-bold shadow-sm" :disabled="isProcessing || cart.length === 0" @click="saveSale">
<span v-if="isProcessing" class="spinner-border spinner-border-sm me-2"></span>
<i class="bi bi-printer me-2" v-else></i>
{% trans "Generate Invoice" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Vue.js 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
createApp({
delimiters: ['[[', ']]'],
data() {
return {
products: [
{% for p in products %}
{
id: {{ p.id }},
name_en: "{{ p.name_en }}",
name_ar: "{{ p.name_ar }}",
sku: "{{ p.sku }}",
price: {{ p.price }},
stock: {{ p.stock_quantity }}
},
{% endfor %}
],
searchQuery: '',
filteredProducts: [],
cart: [],
customerId: '',
invoiceNumber: '',
paymentType: 'cash',
paidAmount: 0,
discount: 0,
dueDate: '',
notes: '',
currencySymbol: '{{ site_settings.currency_symbol }}',
isProcessing: false
}
},
computed: {
subtotal() {
return this.cart.reduce((total, item) => total + (item.price * item.quantity), 0);
},
grandTotal() {
return Math.max(0, this.subtotal - this.discount);
}
},
methods: {
filterProducts() {
if (this.searchQuery.length > 1) {
const query = this.searchQuery.toLowerCase();
this.filteredProducts = this.products.filter(p =>
p.name_en.toLowerCase().includes(query) ||
p.sku.toLowerCase().includes(query) ||
p.name_ar.includes(query)
).slice(0, 5);
} else {
this.filteredProducts = [];
}
},
addItem(product) {
const existing = this.cart.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.cart.push({
id: product.id,
name_en: product.name_en,
sku: product.sku,
price: product.price,
quantity: 1
});
}
this.searchQuery = '';
this.filteredProducts = [];
},
removeItem(index) {
this.cart.splice(index, 1);
},
saveSale() {
this.isProcessing = true;
let actualPaidAmount = 0;
if (this.paymentType === 'cash') {
actualPaidAmount = this.grandTotal;
} else if (this.paymentType === 'partial') {
actualPaidAmount = this.paidAmount;
}
const payload = {
customer_id: this.customerId,
invoice_number: this.invoiceNumber,
items: this.cart.map(item => ({
id: item.id,
quantity: item.quantity,
price: item.price,
line_total: item.price * item.quantity
})),
total_amount: this.grandTotal,
discount: this.discount,
paid_amount: actualPaidAmount,
payment_type: this.paymentType,
due_date: this.dueDate,
notes: this.notes
};
fetch("{% url 'create_sale_api' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.location.href = "{% url 'invoices' %}";
} else {
alert("Error: " + data.error);
this.isProcessing = false;
}
})
.catch(err => {
console.error(err);
alert("An unexpected error occurred.");
this.isProcessing = false;
});
}
}
}).mount('#saleApp');
</script>
{% endblock %}

View File

@ -0,0 +1,195 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Sales Invoice" %} #{{ sale.invoice_number|default:sale.id }} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container py-4">
<!-- Action Bar -->
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
<a href="{% url 'invoices' %}" class="btn btn-light rounded-3">
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Invoices" %}
</a>
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
<i class="bi bi-printer me-2"></i>{% trans "Print Invoice" %}
</button>
</div>
<!-- Invoice Content -->
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<!-- Header Section -->
<div class="p-5 bg-white">
<div class="row mb-5">
<div class="col-sm-6">
{% if settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo" style="max-height: 80px;" class="mb-4">
{% else %}
<h3 class="fw-bold text-primary mb-4">{{ settings.business_name }}</h3>
{% endif %}
<div class="text-muted small">
<p class="mb-1"><i class="bi bi-geo-alt me-2"></i>{{ settings.address }}</p>
<p class="mb-1"><i class="bi bi-telephone me-2"></i>{{ settings.phone }}</p>
<p class="mb-1"><i class="bi bi-envelope me-2"></i>{{ settings.email }}</p>
{% if settings.vat_number %}
<p class="mb-0"><i class="bi bi-receipt me-2"></i>{% trans "VAT" %}: {{ settings.vat_number }}</p>
{% endif %}
</div>
</div>
<div class="col-sm-6 text-sm-end">
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4">{% trans "Tax Invoice" %}</h1>
<div class="mb-4">
<div class="fw-bold text-dark">{% trans "Invoice Number" %}</div>
<div class="h5">{{ sale.invoice_number|default:sale.id }}</div>
</div>
<div class="row g-3">
<div class="col-6">
<div class="small text-muted fw-bold">{% trans "Issue Date" %}</div>
<div>{{ sale.created_at|date:"Y-m-d" }}</div>
</div>
<div class="col-6">
<div class="small text-muted fw-bold">{% trans "Due Date" %}</div>
<div>{{ sale.due_date|date:"Y-m-d"|default:"-" }}</div>
</div>
</div>
</div>
</div>
<div class="row mb-5">
<div class="col-sm-6">
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Customer Information" %}</div>
<div class="h5 fw-bold mb-1">{{ sale.customer.name|default:_("Guest Customer") }}</div>
{% if sale.customer.phone %}
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ sale.customer.phone }}</div>
{% endif %}
{% if sale.customer.address %}
<div class="text-muted small"><i class="bi bi-geo-alt me-2"></i>{{ sale.customer.address }}</div>
{% endif %}
</div>
<div class="col-sm-6 text-sm-end">
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Payment Status" %}</div>
<div>
{% if sale.status == 'paid' %}
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %}</span>
{% elif sale.status == 'partial' %}
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %}</span>
{% else %}
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %}</span>
{% endif %}
</div>
</div>
</div>
<!-- Table Section -->
<div class="table-responsive mb-5">
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr>
<th class="py-3 ps-4 border-0">{% trans "Item Description" %}</th>
<th class="py-3 text-center border-0">{% trans "Unit Price" %}</th>
<th class="py-3 text-center border-0">{% trans "Quantity" %}</th>
<th class="py-3 text-end pe-4 border-0">{% trans "Total" %}</th>
</tr>
</thead>
<tbody>
{% for item in sale.items.all %}
<tr>
<td class="py-3 ps-4">
<div class="fw-bold">{{ item.product.name_en }}</div>
<div class="text-muted small">{{ item.product.name_ar }}</div>
</td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}</td>
<td class="py-3 text-center">{{ item.quantity }}</td>
<td class="py-3 text-end pe-4 fw-bold text-primary">{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="2" class="border-0"></td>
<td class="text-center py-3 fw-bold border-top">{% trans "Subtotal" %}</td>
<td class="text-end pe-4 py-3 fw-bold border-top">{{ settings.currency_symbol }}{{ sale.total_amount|add:sale.discount|floatformat:3 }}</td>
</tr>
{% if sale.discount > 0 %}
<tr class="text-muted">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold">{% trans "Discount" %}</td>
<td class="text-end pe-4 py-2 fw-bold">-{{ settings.currency_symbol }}{{ sale.discount|floatformat:3 }}</td>
</tr>
{% endif %}
<tr>
<td colspan="2" class="border-0"></td>
<td class="text-center py-3 fw-bold">{% trans "Grand Total" %}</td>
<td class="text-end pe-4 py-3 h5 fw-bold text-primary">{{ settings.currency_symbol }}{{ sale.total_amount|floatformat:3 }}</td>
</tr>
<tr class="text-success">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold">{% trans "Total Paid" %}</td>
<td class="text-end pe-4 py-2 fw-bold">{{ settings.currency_symbol }}{{ sale.paid_amount|floatformat:3 }}</td>
</tr>
<tr class="text-danger">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold border-top">{% trans "Balance Due" %}</td>
<td class="text-end pe-4 py-2 h5 fw-bold border-top">{{ settings.currency_symbol }}{{ sale.balance_due|floatformat:3 }}</td>
</tr>
</tfoot>
</table>
</div>
<!-- Payment History -->
{% if sale.payments.exists %}
<div class="mb-5">
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment Records" %}</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="bg-light small">
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Amount" %}</th>
<th>{% trans "Notes" %}</th>
</tr>
</thead>
<tbody class="small">
{% for payment in sale.payments.all %}
<tr>
<td>{{ payment.payment_date|date:"Y-m-d" }}</td>
<td>{{ payment.payment_method }}</td>
<td class="fw-bold">{{ settings.currency_symbol }}{{ payment.amount|floatformat:3 }}</td>
<td class="text-muted">{{ payment.notes }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Notes -->
{% if sale.notes %}
<div class="bg-light p-4 rounded-3 mb-5">
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Internal Notes" %}</h6>
<p class="mb-0 small">{{ sale.notes }}</p>
</div>
{% endif %}
<div class="text-center text-muted small mt-5 border-top pt-4">
<p class="mb-1">{% trans "Thank you for your business!" %}</p>
<p class="mb-0">{% trans "Software by Meezan" %}</p>
</div>
</div>
</div>
</div>
</div>
<style>
@media print {
@page { size: portrait; margin: 0; }
body { background-color: white !important; }
.container { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; }
.card { box-shadow: none !important; border: none !important; }
.d-print-none { display: none !important; }
.p-5 { padding: 2rem !important; }
}
</style>
{% endblock %}

View File

@ -0,0 +1,160 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Sales Invoices" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0">{% trans "Sales Invoices" %}</h2>
<p class="text-muted small mb-0">{% trans "Track and manage your customer sales" %}</p>
</div>
<a href="{% url 'invoice_create' %}" class="btn btn-primary rounded-3 px-4 shadow-sm">
<i class="bi bi-plus-circle me-2"></i>{% trans "Create Sales Invoice" %}
</a>
</div>
{% if messages %}
<div class="mb-4">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3 shadow-sm border-0" role="alert">
<i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
<div class="card border-0 shadow-sm rounded-4">
<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 "Invoice #" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Customer" %}</th>
<th>{% trans "Total" %}</th>
<th>{% trans "Paid" %}</th>
<th>{% trans "Balance" %}</th>
<th>{% trans "Status" %}</th>
<th class="text-end pe-4">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for sale in sales %}
<tr>
<td class="ps-4 fw-bold">
{{ sale.invoice_number|default:sale.id }}
</td>
<td>{{ sale.created_at|date:"Y-m-d" }}</td>
<td>{{ sale.customer.name|default:_("Guest") }}</td>
<td class="fw-bold text-dark">{{ site_settings.currency_symbol }}{{ sale.total_amount|floatformat:3 }}</td>
<td class="text-success">{{ site_settings.currency_symbol }}{{ sale.paid_amount|floatformat:3 }}</td>
<td class="text-danger fw-bold">
{% if sale.balance_due > 0 %}
{{ site_settings.currency_symbol }}{{ sale.balance_due|floatformat:3 }}
{% else %}
<span class="text-success">0.000</span>
{% endif %}
</td>
<td>
{% if sale.status == 'paid' %}
<span class="badge bg-success-subtle text-success rounded-pill px-3">{% trans "Paid" %}</span>
{% elif sale.status == 'partial' %}
<span class="badge bg-warning-subtle text-warning rounded-pill px-3">{% trans "Partial" %}</span>
{% else %}
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">{% trans "Unpaid" %}</span>
{% endif %}
</td>
<td class="text-end pe-4">
<div class="btn-group shadow-sm rounded-3">
<a href="{% url 'invoice_detail' sale.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
<i class="bi bi-printer"></i>
</a>
{% if sale.balance_due > 0 %}
<button type="button" class="btn btn-sm btn-white border" data-bs-toggle="modal" data-bs-target="#paymentModal{{ sale.id }}" title="{% trans 'Record Payment' %}">
<i class="bi bi-cash-stack"></i>
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-white border text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ sale.id }}">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- Payment Modal -->
<div class="modal fade text-start" id="paymentModal{{ sale.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0">
<h5 class="fw-bold">{% trans "Record Customer Payment" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="{% url 'add_sale_payment' sale.id %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="alert alert-info border-0 rounded-3 small">
{% trans "Remaining Balance" %}: <strong>{{ site_settings.currency_symbol }}{{ sale.balance_due|floatformat:3 }}</strong>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Amount" %}</label>
<input type="number" step="0.001" name="amount" max="{{ sale.balance_due }}" value="{{ sale.balance_due }}" class="form-control rounded-3" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Payment Method" %}</label>
<select name="payment_method" class="form-select rounded-3">
<option value="Cash">{% trans "Cash" %}</option>
<option value="Bank Transfer">{% trans "Bank Transfer" %}</option>
<option value="Cheque">{% trans "Cheque" %}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Notes" %}</label>
<textarea name="notes" class="form-control rounded-3" rows="2"></textarea>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Payment" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade text-start" id="deleteModal{{ sale.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-body p-4 text-center">
<div class="text-danger mb-3">
<i class="bi bi-exclamation-octagon" style="font-size: 3rem;"></i>
</div>
<h4 class="fw-bold">{% trans "Delete Sales Invoice?" %}</h4>
<p class="text-muted">{% trans "This will restore the product quantities and delete all payment history. This action cannot be undone." %}</p>
<div class="d-grid gap-2">
<a href="{% url 'delete_sale' sale.id %}" class="btn btn-danger rounded-3 py-2">{% trans "Yes, Delete" %}</a>
<button type="button" class="btn btn-light rounded-3 py-2" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center py-5">
<img src="https://illustrations.popsy.co/gray/success.svg" alt="Empty" style="width: 200px;" class="mb-3">
<p class="text-muted">{% trans "No sales invoices found." %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -48,21 +48,32 @@
visibility: hidden; visibility: hidden;
} }
#invoice-print, #invoice-print * { #invoice-print, #invoice-print * {
visibility: visible; visibility: visible !important;
} }
#invoice-print { #invoice-print {
position: absolute; display: block !important;
position: fixed;
left: 0; left: 0;
top: 0; top: 0;
width: 80mm; width: 80mm;
padding: 5mm; padding: 5mm;
margin: 0;
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
font-size: 12px; font-size: 11px;
color: black; color: black;
background: white;
z-index: 9999;
} }
.no-print { .no-print {
display: none !important; display: none !important;
} }
#sidebar, .top-navbar, .btn, .modal {
display: none !important;
}
main {
padding: 0 !important;
margin: 0 !important;
}
} }
#invoice-print { #invoice-print {
@ -75,19 +86,19 @@
} }
.invoice-header { text-align: center; margin-bottom: 10px; border-bottom: 1px dashed #000; padding-bottom: 10px; } .invoice-header { text-align: center; margin-bottom: 10px; border-bottom: 1px dashed #000; padding-bottom: 10px; }
.invoice-title { font-size: 16px; font-weight: bold; margin-bottom: 5px; } .invoice-title { font-size: 16px; font-weight: bold; margin-bottom: 5px; }
.invoice-details { margin-bottom: 10px; font-size: 11px; } .invoice-details { margin-bottom: 10px; font-size: 10px; }
.invoice-table { width: 100%; border-collapse: collapse; margin-bottom: 10px; } .invoice-table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
.invoice-table th { border-bottom: 1px solid #000; text-align: left; padding: 2px 0; font-size: 11px; } .invoice-table th { border-bottom: 1px solid #000; text-align: left; padding: 2px 0; font-size: 10px; }
.invoice-table td { padding: 4px 0; font-size: 11px; vertical-align: top; } .invoice-table td { padding: 4px 0; font-size: 10px; vertical-align: top; }
.invoice-total { border-top: 1px dashed #000; padding-top: 5px; } .invoice-total { border-top: 1px dashed #000; padding-top: 5px; }
.bilingual { display: flex; justify-content: space-between; font-size: 10px; color: #555; } .bilingual { display: flex; justify-content: space-between; font-size: 9px; color: #555; }
.rtl { direction: rtl; text-align: right; } .rtl { direction: rtl; text-align: right; }
.ltr { direction: ltr; text-align: left; } .ltr { direction: ltr; text-align: left; }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid px-4"> <div class="container-fluid px-4 no-print">
<div class="row g-4"> <div class="row g-4">
<!-- Products Section --> <!-- Products Section -->
<div class="col-lg-8"> <div class="col-lg-8">
@ -113,7 +124,7 @@
<div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}"> <div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}">
<div class="card h-100 shadow-sm product-card p-2" onclick="addToCart({{ product.id }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price }})"> <div class="card h-100 shadow-sm product-card p-2" onclick="addToCart({{ product.id }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price }})">
{% if product.image %} {% if product.image %}
<img src="{{ product.image }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 150px; object-fit: cover;"> <img src="{{ product.image.url }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 150px; object-fit: cover;">
{% else %} {% else %}
<div class="bg-light rounded-3 d-flex align-items-center justify-content-center" style="height: 150px;"> <div class="bg-light rounded-3 d-flex align-items-center justify-content-center" style="height: 150px;">
<i class="bi bi-image text-muted opacity-25" style="font-size: 3rem;"></i> <i class="bi bi-image text-muted opacity-25" style="font-size: 3rem;"></i>
@ -123,7 +134,7 @@
<h6 class="fw-bold mb-1"> <h6 class="fw-bold mb-1">
{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %} {% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}
</h6> </h6>
<p class="text-primary fw-bold mb-0">{{ site_settings.currency_symbol }}{{ product.price }}</p> <p class="text-primary fw-bold mb-0">{{ site_settings.currency_symbol }}{{ product.price|floatformat:3 }}</p>
<small class="text-muted">{% trans "Stock" %}: {{ product.stock_quantity }}</small> <small class="text-muted">{% trans "Stock" %}: {{ product.stock_quantity }}</small>
</div> </div>
</div> </div>
@ -158,11 +169,11 @@
<div class="card-footer bg-light border-0 p-4 rounded-bottom-4"> <div class="card-footer bg-light border-0 p-4 rounded-bottom-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span>{% trans "Subtotal" %}</span> <span>{% trans "Subtotal" %}</span>
<span id="subtotalAmount">{{ site_settings.currency_symbol }}0.00</span> <span id="subtotalAmount">{{ site_settings.currency_symbol }}0.000</span>
</div> </div>
<div class="d-flex justify-content-between mb-3"> <div class="d-flex justify-content-between mb-3">
<span class="fw-bold fs-5">{% trans "Total" %}</span> <span class="fw-bold fs-5">{% trans "Total" %}</span>
<span class="fw-bold fs-5 text-primary" id="totalAmount">{{ site_settings.currency_symbol }}0.00</span> <span class="fw-bold fs-5 text-primary" id="totalAmount">{{ site_settings.currency_symbol }}0.000</span>
</div> </div>
<button id="payNowBtn" class="btn btn-primary w-100 py-3 fw-bold rounded-3" onclick="checkout()" disabled> <button id="payNowBtn" class="btn btn-primary w-100 py-3 fw-bold rounded-3" onclick="checkout()" disabled>
{% trans "PAY NOW" %} {% trans "PAY NOW" %}
@ -176,10 +187,24 @@
<!-- Invoice Template (Hidden) --> <!-- Invoice Template (Hidden) -->
<div id="invoice-print"> <div id="invoice-print">
<div class="invoice-header"> <div class="invoice-header">
<div id="inv-logo-container" class="mb-2">
<img id="inv-logo" src="" alt="Logo" style="max-height: 50px; display: none;">
</div>
<div class="invoice-title" id="inv-business-name"></div> <div class="invoice-title" id="inv-business-name"></div>
<div class="bilingual"><span class="ltr">TAX INVOICE</span><span class="rtl">فاتورة ضريبية</span></div> <div class="bilingual"><span class="ltr">TAX INVOICE</span><span class="rtl">فاتورة ضريبية</span></div>
<div id="inv-business-address" style="font-size: 10px;"></div> <div id="inv-business-address" style="font-size: 10px;"></div>
<div id="inv-business-phone" style="font-size: 10px;"></div> <div id="inv-business-phone" style="font-size: 10px;"></div>
<div id="inv-business-email" style="font-size: 10px;"></div>
<div class="mt-1" style="border-top: 1px solid #eee; padding-top: 2px;">
<div class="bilingual">
<span>VAT No / الرقم الضريبي:</span>
<span id="inv-vat-no"></span>
</div>
<div class="bilingual">
<span>CR No / السجل التجاري:</span>
<span id="inv-cr-no"></span>
</div>
</div>
</div> </div>
<div class="invoice-details"> <div class="invoice-details">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
@ -215,7 +240,7 @@
</div> </div>
<!-- Receipt Modal --> <!-- Receipt Modal -->
<div class="modal fade" id="receiptModal" tabindex="-1"> <div class="modal fade no-print" id="receiptModal" tabindex="-1">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body text-center p-4"> <div class="modal-body text-center p-4">
@ -240,6 +265,11 @@
let lastSaleData = null; let lastSaleData = null;
const lang = '{{ LANGUAGE_CODE }}'; const lang = '{{ LANGUAGE_CODE }}';
const currency = '{{ site_settings.currency_symbol }}'; const currency = '{{ site_settings.currency_symbol }}';
const decimalPlaces = currency === 'OMR' ? 3 : 2;
function formatAmount(amount) {
return parseFloat(amount).toFixed(decimalPlaces);
}
function addToCart(id, nameEn, nameAr, price) { function addToCart(id, nameEn, nameAr, price) {
const existing = cart.find(item => item.id === id); const existing = cart.find(item => item.id === id);
@ -281,8 +311,8 @@
if (cart.length === 0) { if (cart.length === 0) {
emptyMsg.classList.remove('d-none'); emptyMsg.classList.remove('d-none');
listContainer.innerHTML = ''; listContainer.innerHTML = '';
document.getElementById('subtotalAmount').innerText = `${currency}0.00`; document.getElementById('subtotalAmount').innerText = `${currency} ${formatAmount(0)}`;
document.getElementById('totalAmount').innerText = `${currency}0.00`; document.getElementById('totalAmount').innerText = `${currency} ${formatAmount(0)}`;
payBtn.disabled = true; payBtn.disabled = true;
return; return;
} }
@ -298,7 +328,7 @@
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<div> <div>
<div class="fw-bold small">${item.name}</div> <div class="fw-bold small">${item.name}</div>
<div class="text-muted small">${currency}${item.price} x ${item.quantity}</div> <div class="text-muted small">${currency} ${formatAmount(item.price)} x ${item.quantity}</div>
</div> </div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<button class="btn btn-sm btn-outline-secondary rounded-circle" onclick="updateQuantity(${item.id}, -1)">-</button> <button class="btn btn-sm btn-outline-secondary rounded-circle" onclick="updateQuantity(${item.id}, -1)">-</button>
@ -310,8 +340,8 @@
}); });
listContainer.innerHTML = html; listContainer.innerHTML = html;
document.getElementById('subtotalAmount').innerText = `${currency}${total.toFixed(2)}`; document.getElementById('subtotalAmount').innerText = `${currency} ${formatAmount(total)}`;
document.getElementById('totalAmount').innerText = `${currency}${total.toFixed(2)}`; document.getElementById('totalAmount').innerText = `${currency} ${formatAmount(total)}`;
} }
function checkout() { function checkout() {
@ -366,9 +396,23 @@
} }
function prepareInvoice(data) { function prepareInvoice(data) {
document.getElementById('inv-business-name').innerText = data.business.name; console.log('Preparing invoice with data:', data);
document.getElementById('inv-business-address').innerText = data.business.address;
document.getElementById('inv-business-phone').innerText = 'Tel: ' + data.business.phone; const logo = document.getElementById('inv-logo');
if (data.business.logo_url) {
logo.src = data.business.logo_url;
logo.style.display = 'inline-block';
} else {
logo.style.display = 'none';
}
document.getElementById('inv-business-name').innerText = data.business.name || 'Business Name';
document.getElementById('inv-business-address').innerText = data.business.address || '';
document.getElementById('inv-business-phone').innerText = data.business.phone ? 'Tel: ' + data.business.phone : '';
document.getElementById('inv-business-email').innerText = data.business.email ? 'Email: ' + data.business.email : '';
document.getElementById('inv-vat-no').innerText = data.business.vat_number || '---';
document.getElementById('inv-cr-no').innerText = data.business.registration_number || '---';
document.getElementById('inv-id').innerText = data.sale.id; document.getElementById('inv-id').innerText = data.sale.id;
document.getElementById('inv-id-ar').innerText = data.sale.id; document.getElementById('inv-id-ar').innerText = data.sale.id;
document.getElementById('inv-date').innerText = data.sale.created_at; document.getElementById('inv-date').innerText = data.sale.created_at;
@ -383,15 +427,16 @@
<div class="rtl text-muted" style="font-size: 9px;">${item.name_ar}</div> <div class="rtl text-muted" style="font-size: 9px;">${item.name_ar}</div>
</td> </td>
<td style="text-align: center;">${item.qty}</td> <td style="text-align: center;">${item.qty}</td>
<td style="text-align: right;">${data.business.currency}${item.total.toFixed(2)}</td> <td style="text-align: right;">${data.business.currency} ${formatAmount(item.total)}</td>
</tr> </tr>
`; `;
}); });
document.getElementById('inv-items').innerHTML = itemsHtml; document.getElementById('inv-items').innerHTML = itemsHtml;
document.getElementById('inv-total').innerText = data.business.currency + data.sale.total.toFixed(2); document.getElementById('inv-total').innerText = data.business.currency + ' ' + formatAmount(data.sale.total);
} }
function printInvoice() { function printInvoice() {
console.log('Printing invoice...');
window.print(); window.print();
} }
@ -425,4 +470,4 @@
}); });
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,275 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "New Purchase" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container-fluid px-4" id="purchaseApp">
<div class="row">
<!-- Main Form -->
<div class="col-lg-8">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-header bg-white border-0 pt-4 px-4">
<h5 class="fw-bold mb-0"><i class="bi bi-cart-plus me-2 text-primary"></i>{% trans "Create Purchase Invoice" %}</h5>
</div>
<div class="card-body p-4">
<!-- Supplier & Invoice Info -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="supplierId">
<option value="">{% trans "Select Supplier" %}</option>
{% for supplier in suppliers %}
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Reference / Invoice #" %}</label>
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="invoiceNumber" placeholder="{% trans 'e.g. INV-2024-001' %}">
</div>
</div>
<!-- Item Selection -->
<div class="mb-4">
<label class="form-label small fw-bold">{% trans "Add Items to Invoice" %}</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0 border-secondary-subtle"><i class="bi bi-search"></i></span>
<input type="text" class="form-control rounded-3 border-start-0 border-secondary-subtle shadow-none" placeholder="{% trans 'Search by Name or SKU...' %}" v-model="searchQuery" @input="filterProducts">
</div>
<div class="position-relative">
<div class="list-group position-absolute w-100 shadow rounded-3 mt-1" style="z-index: 1000;" v-if="filteredProducts.length > 0">
<button v-for="product in filteredProducts" :key="product.id" class="list-group-item list-group-item-action border-0 py-3" @click="addItem(product)">
<div class="d-flex justify-content-between">
<div>
<span class="fw-bold">[[ product.name_en ]]</span> / [[ product.name_ar ]]
<div class="text-muted small">SKU: [[ product.sku ]] | Stock: [[ product.stock ]]</div>
</div>
<div class="text-primary fw-bold">[[ currencySymbol ]][[ product.cost_price ]]</div>
</div>
</button>
</div>
</div>
</div>
<!-- Items Table -->
<div class="table-responsive">
<table class="table align-middle">
<thead class="bg-light-subtle">
<tr class="small text-uppercase text-muted fw-bold">
<th style="width: 40%;">{% trans "Product" %}</th>
<th class="text-center">{% trans "Cost Price" %}</th>
<th class="text-center" style="width: 15%;">{% trans "Quantity" %}</th>
<th class="text-end">{% trans "Total" %}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in cart" :key="index">
<td>
<div class="fw-bold">[[ item.name_en ]]</div>
<div class="text-muted small">[[ item.sku ]]</div>
</td>
<td>
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
<td class="text-end">
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
</td>
</tr>
<tr v-if="cart.length === 0">
<td colspan="5" class="text-center py-5 text-muted">
{% trans "No items added yet." %}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Order Summary -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm rounded-4 sticky-top" style="top: 20px;">
<div class="card-body p-4">
<h5 class="fw-bold mb-4">{% trans "Purchase Summary" %}</h5>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">{% trans "Subtotal" %}</span>
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</span>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between mb-4">
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</h4>
</div>
<!-- Payment Details -->
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Payment Type" %}</label>
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="paymentType">
<option value="cash">{% trans "Full Cash" %}</option>
<option value="credit">{% trans "Full Credit" %}</option>
<option value="partial">{% trans "Partial Payment" %}</option>
</select>
</div>
<div class="mb-3" v-if="paymentType === 'partial'">
<label class="form-label small fw-bold">{% trans "Amount Paid" %}</label>
<input type="number" step="0.001" class="form-control rounded-3" v-model="paidAmount">
</div>
<div class="mb-4" v-if="paymentType !== 'cash'">
<label class="form-label small fw-bold">{% trans "Due Date" %}</label>
<input type="date" class="form-control rounded-3" v-model="dueDate">
</div>
<div class="mb-4">
<label class="form-label small fw-bold">{% trans "Notes" %}</label>
<textarea class="form-control rounded-3" rows="2" v-model="notes"></textarea>
</div>
<div class="d-grid">
<button class="btn btn-primary rounded-3 py-3 fw-bold shadow-sm" :disabled="isProcessing || cart.length === 0 || !supplierId" @click="savePurchase">
<span v-if="isProcessing" class="spinner-border spinner-border-sm me-2"></span>
<i class="bi bi-check2-circle me-2" v-else></i>
{% trans "Finalize Purchase" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Vue.js 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
createApp({
delimiters: ['[[', ']]'],
data() {
return {
products: [
{% for p in products %}
{
id: {{ p.id }},
name_en: "{{ p.name_en }}",
name_ar: "{{ p.name_ar }}",
sku: "{{ p.sku }}",
cost_price: {{ p.cost_price }},
stock: {{ p.stock_quantity }}
},
{% endfor %}
],
searchQuery: '',
filteredProducts: [],
cart: [],
supplierId: '',
invoiceNumber: '',
paymentType: 'cash',
paidAmount: 0,
dueDate: '',
notes: '',
currencySymbol: '{{ site_settings.currency_symbol }}',
isProcessing: false
}
},
computed: {
subtotal() {
return this.cart.reduce((total, item) => total + (item.price * item.quantity), 0);
}
},
methods: {
filterProducts() {
if (this.searchQuery.length > 1) {
const query = this.searchQuery.toLowerCase();
this.filteredProducts = this.products.filter(p =>
p.name_en.toLowerCase().includes(query) ||
p.sku.toLowerCase().includes(query) ||
p.name_ar.includes(query)
).slice(0, 5);
} else {
this.filteredProducts = [];
}
},
addItem(product) {
const existing = this.cart.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.cart.push({
id: product.id,
name_en: product.name_en,
sku: product.sku,
price: product.cost_price,
quantity: 1
});
}
this.searchQuery = '';
this.filteredProducts = [];
},
removeItem(index) {
this.cart.splice(index, 1);
},
savePurchase() {
this.isProcessing = true;
let actualPaidAmount = 0;
if (this.paymentType === 'cash') {
actualPaidAmount = this.subtotal;
} else if (this.paymentType === 'partial') {
actualPaidAmount = this.paidAmount;
}
const payload = {
supplier_id: this.supplierId,
invoice_number: this.invoiceNumber,
items: this.cart.map(item => ({
id: item.id,
quantity: item.quantity,
price: item.price,
line_total: item.price * item.quantity
})),
total_amount: this.subtotal,
paid_amount: actualPaidAmount,
payment_type: this.paymentType,
due_date: this.dueDate,
notes: this.notes
};
fetch("{% url 'create_purchase_api' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.location.href = "{% url 'purchases' %}";
} else {
alert("Error: " + data.error);
this.isProcessing = false;
}
})
.catch(err => {
console.error(err);
alert("An unexpected error occurred.");
this.isProcessing = false;
});
}
}
}).mount('#purchaseApp');
</script>
{% endblock %}

View File

@ -0,0 +1,182 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Purchase Detail" %} #{{ purchase.id }} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container py-4">
<!-- Action Bar -->
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
<a href="{% url 'purchases' %}" class="btn btn-light rounded-3">
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to List" %}
</a>
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
<i class="bi bi-printer me-2"></i>{% trans "Print Invoice" %}
</button>
</div>
<!-- Invoice Content -->
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<!-- Header Section -->
<div class="p-5 bg-white">
<div class="row mb-5">
<div class="col-sm-6">
{% if settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo" style="max-height: 80px;" class="mb-4">
{% else %}
<h3 class="fw-bold text-primary mb-4">{{ settings.business_name }}</h3>
{% endif %}
<div class="text-muted small">
<p class="mb-1"><i class="bi bi-geo-alt me-2"></i>{{ settings.address }}</p>
<p class="mb-1"><i class="bi bi-telephone me-2"></i>{{ settings.phone }}</p>
<p class="mb-1"><i class="bi bi-envelope me-2"></i>{{ settings.email }}</p>
{% if settings.vat_number %}
<p class="mb-0"><i class="bi bi-receipt me-2"></i>{% trans "VAT" %}: {{ settings.vat_number }}</p>
{% endif %}
</div>
</div>
<div class="col-sm-6 text-sm-end">
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4">{% trans "Purchase Invoice" %}</h1>
<div class="mb-4">
<div class="fw-bold text-dark">{% trans "Invoice Number" %}</div>
<div class="h5">{{ purchase.invoice_number|default:purchase.id }}</div>
</div>
<div class="row g-3">
<div class="col-6">
<div class="small text-muted fw-bold">{% trans "Issue Date" %}</div>
<div>{{ purchase.created_at|date:"Y-m-d" }}</div>
</div>
<div class="col-6">
<div class="small text-muted fw-bold">{% trans "Due Date" %}</div>
<div>{{ purchase.due_date|date:"Y-m-d"|default:"-" }}</div>
</div>
</div>
</div>
</div>
<div class="row mb-5">
<div class="col-sm-6">
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Supplier Information" %}</div>
<div class="h5 fw-bold mb-1">{{ purchase.supplier.name }}</div>
{% if purchase.supplier.phone %}
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ purchase.supplier.phone }}</div>
{% endif %}
{% if purchase.supplier.contact_person %}
<div class="text-muted small"><i class="bi bi-person me-2"></i>{{ purchase.supplier.contact_person }}</div>
{% endif %}
</div>
<div class="col-sm-6 text-sm-end">
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Payment Status" %}</div>
<div>
{% if purchase.status == 'paid' %}
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %}</span>
{% elif purchase.status == 'partial' %}
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %}</span>
{% else %}
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %}</span>
{% endif %}
</div>
</div>
</div>
<!-- Table Section -->
<div class="table-responsive mb-5">
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr>
<th class="py-3 ps-4 border-0">{% trans "Item Description" %}</th>
<th class="py-3 text-center border-0">{% trans "Cost Price" %}</th>
<th class="py-3 text-center border-0">{% trans "Quantity" %}</th>
<th class="py-3 text-end pe-4 border-0">{% trans "Total" %}</th>
</tr>
</thead>
<tbody>
{% for item in purchase.items.all %}
<tr>
<td class="py-3 ps-4">
<div class="fw-bold">{{ item.product.name_en }}</div>
<div class="text-muted small">{{ item.product.name_ar }}</div>
</td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }}</td>
<td class="py-3 text-center">{{ item.quantity }}</td>
<td class="py-3 text-end pe-4 fw-bold text-primary">{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="2" class="border-0"></td>
<td class="text-center py-3 fw-bold border-top">{% trans "Grand Total" %}</td>
<td class="text-end pe-4 py-3 h5 fw-bold text-primary border-top">{{ settings.currency_symbol }}{{ purchase.total_amount|floatformat:3 }}</td>
</tr>
<tr class="text-success">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold">{% trans "Total Paid" %}</td>
<td class="text-end pe-4 py-2 fw-bold">{{ settings.currency_symbol }}{{ purchase.paid_amount|floatformat:3 }}</td>
</tr>
<tr class="text-danger">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold border-top">{% trans "Balance Due" %}</td>
<td class="text-end pe-4 py-2 h5 fw-bold border-top">{{ settings.currency_symbol }}{{ purchase.balance_due|floatformat:3 }}</td>
</tr>
</tfoot>
</table>
</div>
<!-- Payment History -->
{% if purchase.payments.exists %}
<div class="mb-5">
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment History" %}</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="bg-light small">
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Amount" %}</th>
<th>{% trans "Notes" %}</th>
</tr>
</thead>
<tbody class="small">
{% for payment in purchase.payments.all %}
<tr>
<td>{{ payment.payment_date|date:"Y-m-d" }}</td>
<td>{{ payment.payment_method }}</td>
<td class="fw-bold">{{ settings.currency_symbol }}{{ payment.amount|floatformat:3 }}</td>
<td class="text-muted">{{ payment.notes }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Notes -->
{% if purchase.notes %}
<div class="bg-light p-4 rounded-3 mb-5">
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Notes" %}</h6>
<p class="mb-0 small">{{ purchase.notes }}</p>
</div>
{% endif %}
<div class="text-center text-muted small mt-5 border-top pt-4">
<p class="mb-0">{% trans "Thank you for your business!" %}</p>
</div>
</div>
</div>
</div>
</div>
<style>
@media print {
@page { size: portrait; margin: 0; }
body { background-color: white !important; }
.container { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; }
.card { box-shadow: none !important; border: none !important; }
.d-print-none { display: none !important; }
.p-5 { padding: 2rem !important; }
}
</style>
{% endblock %}

View File

@ -1,23 +1,26 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Purchases" %} | Meezan Accounting{% endblock %} {% block title %}{% trans "Stock Purchases" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">{% trans "Purchase History" %}</h2> <div>
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addPurchaseModal"> <h2 class="fw-bold mb-0">{% trans "Purchase Invoices" %}</h2>
<i class="bi bi-cart-plus me-2"></i>{% trans "New Purchase" %} <p class="text-muted small mb-0">{% trans "Manage and track your stock procurement" %}</p>
</button> </div>
<a href="{% url 'purchase_create' %}" class="btn btn-primary rounded-3 px-4 shadow-sm">
<i class="bi bi-plus-circle me-2"></i>{% trans "Create Purchase" %}
</a>
</div> </div>
{% if messages %} {% if messages %}
<div class="mb-4"> <div class="mb-4">
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3 shadow-sm border-0" role="alert">
{{ message }} <i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -29,26 +32,122 @@
<table class="table table-hover align-middle mb-0"> <table class="table table-hover align-middle mb-0">
<thead class="bg-light"> <thead class="bg-light">
<tr> <tr>
<th class="ps-4">{% trans "Date" %}</th> <th class="ps-4">{% trans "Invoice #" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Supplier" %}</th> <th>{% trans "Supplier" %}</th>
<th>{% trans "Total Amount" %}</th> <th>{% trans "Total" %}</th>
<th>{% trans "Paid" %}</th>
<th>{% trans "Balance" %}</th>
<th>{% trans "Status" %}</th>
<th class="text-end pe-4">{% trans "Actions" %}</th> <th class="text-end pe-4">{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for purchase in purchases %} {% for purchase in purchases %}
<tr> <tr>
<td class="ps-4">{{ purchase.created_at|date:"Y-m-d H:i" }}</td> <td class="ps-4 fw-bold">
{{ purchase.invoice_number|default:purchase.id }}
</td>
<td>{{ purchase.created_at|date:"Y-m-d" }}</td>
<td>{{ purchase.supplier.name|default:"-" }}</td> <td>{{ purchase.supplier.name|default:"-" }}</td>
<td class="fw-bold text-primary">{{ site_settings.currency_symbol }}{{ purchase.total_amount }}</td> <td class="fw-bold text-dark">{{ site_settings.currency_symbol }}{{ purchase.total_amount|floatformat:3 }}</td>
<td class="text-success">{{ site_settings.currency_symbol }}{{ purchase.paid_amount|floatformat:3 }}</td>
<td class="text-danger fw-bold">
{% if purchase.balance_due > 0 %}
{{ site_settings.currency_symbol }}{{ purchase.balance_due|floatformat:3 }}
{% else %}
<span class="text-success">0.000</span>
{% endif %}
</td>
<td>
{% if purchase.status == 'paid' %}
<span class="badge bg-success-subtle text-success rounded-pill px-3">{% trans "Paid" %}</span>
{% elif purchase.status == 'partial' %}
<span class="badge bg-warning-subtle text-warning rounded-pill px-3">{% trans "Partial" %}</span>
{% else %}
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">{% trans "Unpaid" %}</span>
{% endif %}
</td>
<td class="text-end pe-4"> <td class="text-end pe-4">
<button class="btn btn-sm btn-light rounded-circle"><i class="bi bi-eye"></i></button> <div class="btn-group shadow-sm rounded-3">
<a href="{% url 'purchase_detail' purchase.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
<i class="bi bi-printer"></i>
</a>
{% if purchase.balance_due > 0 %}
<button type="button" class="btn btn-sm btn-white border" data-bs-toggle="modal" data-bs-target="#paymentModal{{ purchase.id }}" title="{% trans 'Add Payment' %}">
<i class="bi bi-cash-stack"></i>
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-white border text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ purchase.id }}">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- Payment Modal -->
<div class="modal fade text-start" id="paymentModal{{ purchase.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0">
<h5 class="fw-bold">{% trans "Record Payment" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="{% url 'add_purchase_payment' purchase.id %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="alert alert-info border-0 rounded-3 small">
{% trans "Balance Due" %}: <strong>{{ site_settings.currency_symbol }}{{ purchase.balance_due|floatformat:3 }}</strong>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Amount" %}</label>
<input type="number" step="0.001" name="amount" max="{{ purchase.balance_due }}" value="{{ purchase.balance_due }}" class="form-control rounded-3" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Payment Method" %}</label>
<select name="payment_method" class="form-select rounded-3">
<option value="Cash">{% trans "Cash" %}</option>
<option value="Bank Transfer">{% trans "Bank Transfer" %}</option>
<option value="Cheque">{% trans "Cheque" %}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Notes" %}</label>
<textarea name="notes" class="form-control rounded-3" rows="2"></textarea>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Payment" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade text-start" id="deleteModal{{ purchase.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-body p-4 text-center">
<div class="text-danger mb-3">
<i class="bi bi-exclamation-octagon" style="font-size: 3rem;"></i>
</div>
<h4 class="fw-bold">{% trans "Delete Purchase?" %}</h4>
<p class="text-muted">{% trans "This will revert the stock changes and delete all payment history. This action cannot be undone." %}</p>
<div class="d-grid gap-2">
<a href="{% url 'delete_purchase' purchase.id %}" class="btn btn-danger rounded-3 py-2">{% trans "Yes, Delete" %}</a>
<button type="button" class="btn btn-light rounded-3 py-2" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
</div>
</div>
</div>
</div>
</div>
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4" class="text-center py-5 text-muted"> <td colspan="8" class="text-center py-5">
{% trans "No purchases found." %} <img src="https://illustrations.popsy.co/gray/delivery.svg" alt="Empty" style="width: 200px;" class="mb-3">
<p class="text-muted">{% trans "No purchases recorded yet." %}</p>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -58,41 +157,4 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Add Purchase Modal -->
<div class="modal fade" id="addPurchaseModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0">
<h5 class="fw-bold">{% trans "Record New Purchase" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="{% url 'add_purchase' %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
<select name="supplier" class="form-select rounded-3" required>
<option value="">{% trans "Select Supplier" %}</option>
{% for supplier in suppliers %}
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Total Amount" %}</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">{{ site_settings.currency_symbol }}</span>
<input type="number" step="0.01" name="total_amount" class="form-control rounded-3 border-start-0" required>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Record Purchase" %}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Smart Reports" %} | Meezan Accounting{% endblock %} {% block title %}{% trans "Smart Reports" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
@ -29,7 +29,7 @@
{% for sale in monthly_sales %} {% for sale in monthly_sales %}
<tr> <tr>
<td>{{ sale.month|date:"F Y" }}</td> <td>{{ sale.month|date:"F Y" }}</td>
<td class="text-end fw-bold">{{ site_settings.currency_symbol }}{{ sale.total|floatformat:2 }}</td> <td class="text-end fw-bold">{{ site_settings.currency_symbol }}{{ sale.total|floatformat:3 }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
@ -56,7 +56,7 @@
<small class="text-muted">{{ item.total_qty }} {% trans "units sold" %}</small> <small class="text-muted">{{ item.total_qty }} {% trans "units sold" %}</small>
</div> </div>
<div class="text-end"> <div class="text-end">
<span class="d-block fw-bold text-primary">{{ site_settings.currency_symbol }}{{ item.revenue|floatformat:2 }}</span> <span class="d-block fw-bold text-primary">{{ site_settings.currency_symbol }}{{ item.revenue|floatformat:3 }}</span>
</div> </div>
</li> </li>
{% empty %} {% empty %}
@ -67,4 +67,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
@ -30,9 +31,21 @@
<h5 class="card-title mb-0 fw-bold">{% trans "Business Profile" %}</h5> <h5 class="card-title mb-0 fw-bold">{% trans "Business Profile" %}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="row g-3"> <div class="row g-3">
<div class="col-md-12 text-center mb-3">
<label class="form-label d-block fw-semibold">{% trans "Business Logo" %}</label>
{% if settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo" class="img-thumbnail mb-2" style="max-height: 100px;">
{% else %}
<div class="bg-light border rounded d-inline-flex align-items-center justify-content-center mb-2" style="width: 100px; height: 100px;">
<i class="bi bi-image text-muted fs-1"></i>
</div>
{% endif %}
<input type="file" name="logo" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*">
</div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-semibold">{% trans "Business Name" %}</label> <label class="form-label fw-semibold">{% trans "Business Name" %}</label>
<input type="text" name="business_name" class="form-control" value="{{ settings.business_name }}" required> <input type="text" name="business_name" class="form-control" value="{{ settings.business_name }}" required>
@ -45,6 +58,14 @@
<label class="form-label fw-semibold">{% trans "Phone Number" %}</label> <label class="form-label fw-semibold">{% trans "Phone Number" %}</label>
<input type="text" name="phone" class="form-control" value="{{ settings.phone }}"> <input type="text" name="phone" class="form-control" value="{{ settings.phone }}">
</div> </div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "VAT Number" %}</label>
<input type="text" name="vat_number" class="form-control" value="{{ settings.vat_number }}">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Registration Number" %}</label>
<input type="text" name="registration_number" class="form-control" value="{{ settings.registration_number }}">
</div>
<div class="col-12"> <div class="col-12">
<label class="form-label fw-semibold">{% trans "Address" %}</label> <label class="form-label fw-semibold">{% trans "Address" %}</label>
<textarea name="address" class="form-control" rows="3">{{ settings.address }}</textarea> <textarea name="address" class="form-control" rows="3">{{ settings.address }}</textarea>
@ -56,7 +77,7 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Currency Symbol" %}</label> <label class="form-label fw-semibold">{% trans "Currency Symbol" %}</label>
<input type="text" name="currency_symbol" class="form-control" value="{{ settings.currency_symbol }}" required> <input type="text" name="currency_symbol" class="form-control" value="{{ settings.currency_symbol }}" required>
<div class="form-text">{% trans "e.g., $, £, SAR, AED" %}</div> <div class="form-text">{% trans "e.g., OMR, $, £, SAR" %}</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Default Tax Rate (%)" %}</label> <label class="form-label fw-semibold">{% trans "Default Tax Rate (%)" %}</label>
@ -99,4 +120,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -6,8 +6,16 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">{% trans "Suppliers" %}</h2> <div>
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addSupplierModal"> <h2 class="fw-bold mb-1">{% trans "Suppliers" %}</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'index' %}">{% trans "Dashboard" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Suppliers" %}</li>
</ol>
</nav>
</div>
<button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addSupplierModal">
<i class="bi bi-truck me-2"></i>{% trans "Add Supplier" %} <i class="bi bi-truck me-2"></i>{% trans "Add Supplier" %}
</button> </button>
</div> </div>
@ -17,7 +25,7 @@
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -29,7 +37,7 @@
<table class="table table-hover align-middle mb-0"> <table class="table table-hover align-middle mb-0">
<thead class="bg-light"> <thead class="bg-light">
<tr> <tr>
<th class="ps-4">{% trans "Name" %}</th> <th class="ps-4">{% trans "Supplier" %}</th>
<th>{% trans "Contact Person" %}</th> <th>{% trans "Contact Person" %}</th>
<th>{% trans "Phone" %}</th> <th>{% trans "Phone" %}</th>
<th class="text-end pe-4">{% trans "Actions" %}</th> <th class="text-end pe-4">{% trans "Actions" %}</th>
@ -38,18 +46,92 @@
<tbody> <tbody>
{% for supplier in suppliers %} {% for supplier in suppliers %}
<tr> <tr>
<td class="ps-4 fw-bold">{{ supplier.name }}</td> <td class="ps-4 fw-bold text-dark">{{ supplier.name }}</td>
<td>{{ supplier.contact_person }}</td> <td>{{ supplier.contact_person|default:"-" }}</td>
<td>{{ supplier.phone }}</td> <td>{{ supplier.phone|default:"-" }}</td>
<td class="text-end pe-4"> <td class="text-end pe-4">
<button class="btn btn-sm btn-light rounded-circle"><i class="bi bi-pencil"></i></button> <div class="btn-group">
<button class="btn btn-sm btn-light rounded-circle text-danger"><i class="bi bi-trash"></i></button> <button class="btn btn-sm btn-light rounded-pill px-2 me-1" data-bs-toggle="modal" data-bs-target="#viewSupplierModal{{ supplier.id }}" title="{% trans 'View' %}">
<i class="bi bi-eye text-primary"></i>
</button>
<button class="btn btn-sm btn-light rounded-pill px-2 me-1" data-bs-toggle="modal" data-bs-target="#editSupplierModal{{ supplier.id }}" title="{% trans 'Edit' %}">
<i class="bi bi-pencil text-success"></i>
</button>
<a href="{% url 'delete_supplier' supplier.id %}" class="btn btn-sm btn-light rounded-pill px-2" onclick="return confirm('{% trans 'Are you sure you want to delete this supplier?' %}')" title="{% trans 'Delete' %}">
<i class="bi bi-trash text-danger"></i>
</a>
</div>
</td> </td>
</tr> </tr>
<!-- View Supplier Modal -->
<div class="modal fade" id="viewSupplierModal{{ supplier.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Supplier Details" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-center mb-4">
<div class="bg-primary bg-opacity-10 text-primary rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
<i class="bi bi-truck" style="font-size: 2.5rem;"></i>
</div>
<h4 class="fw-bold mb-0">{{ supplier.name }}</h4>
</div>
<hr>
<div class="row g-3">
<div class="col-6">
<small class="text-muted d-block">{% trans "Contact Person" %}</small>
<span class="fw-bold">{{ supplier.contact_person|default:"-" }}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Phone" %}</small>
<span class="fw-bold">{{ supplier.phone|default:"-" }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Supplier Modal -->
<div class="modal fade" id="editSupplierModal{{ supplier.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Edit Supplier" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'edit_supplier' supplier.id %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Supplier Name" %}</label>
<input type="text" name="name" class="form-control rounded-3" value="{{ supplier.name }}" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Contact Person" %}</label>
<input type="text" name="contact_person" class="form-control rounded-3" value="{{ supplier.contact_person }}">
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Phone Number" %}</label>
<input type="text" name="phone" class="form-control rounded-3" value="{{ supplier.phone }}">
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Update Supplier" %}</button>
</div>
</form>
</div>
</div>
</div>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4" class="text-center py-5 text-muted"> <td colspan="4" class="text-center py-5">
{% trans "No suppliers found." %} <i class="bi bi-truck text-muted opacity-25" style="font-size: 3rem;"></i>
<p class="mt-2 text-muted">{% trans "No suppliers found." %}</p>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -61,12 +143,12 @@
</div> </div>
<!-- Add Supplier Modal --> <!-- Add Supplier Modal -->
<div class="modal fade" id="addSupplierModal" tabindex="-1"> <div class="modal fade" id="addSupplierModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4"> <div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0"> <div class="modal-header border-0 pb-0">
<h5 class="fw-bold">{% trans "Add New Supplier" %}</h5> <h5 class="modal-title fw-bold">{% trans "Add New Supplier" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form action="{% url 'add_supplier' %}" method="POST"> <form action="{% url 'add_supplier' %}" method="POST">
{% csrf_token %} {% csrf_token %}

View File

@ -11,10 +11,45 @@ urlpatterns = [
path('reports/', views.reports, name='reports'), path('reports/', views.reports, name='reports'),
path('settings/', views.settings_view, name='settings'), path('settings/', views.settings_view, name='settings'),
# Invoices (Sales)
path('invoices/', views.invoice_list, name='invoices'), # Changed to 'invoices' for consistency with sidebar
path('invoices/create/', views.invoice_create, name='invoice_create'),
path('invoices/<int:pk>/', views.invoice_detail, name='invoice_detail'),
path('invoices/payment/<int:pk>/', views.add_sale_payment, name='add_sale_payment'),
path('invoices/delete/<int:pk>/', views.delete_sale, name='delete_sale'),
# Purchases (Invoices)
path('purchases/create/', views.purchase_create, name='purchase_create'),
path('purchases/<int:pk>/', views.purchase_detail, name='purchase_detail'),
path('purchases/payment/<int:pk>/', views.add_purchase_payment, name='add_purchase_payment'),
path('purchases/delete/<int:pk>/', views.delete_purchase, name='delete_purchase'),
# API / Actions # API / Actions
path('api/create-sale/', views.create_sale_api, name='create_sale_api'), path('api/create-sale/', views.create_sale_api, name='create_sale_api'),
path('api/create-purchase/', views.create_purchase_api, name='create_purchase_api'),
# Customers
path('customers/add/', views.add_customer, name='add_customer'), path('customers/add/', views.add_customer, name='add_customer'),
path('customers/edit/<int:pk>/', views.edit_customer, name='edit_customer'),
path('customers/delete/<int:pk>/', views.delete_customer, name='delete_customer'),
# Suppliers
path('suppliers/add/', views.add_supplier, name='add_supplier'), path('suppliers/add/', views.add_supplier, name='add_supplier'),
path('purchases/add/', views.add_purchase, name='add_purchase'), path('suppliers/edit/<int:pk>/', views.edit_supplier, name='edit_supplier'),
path('suppliers/delete/<int:pk>/', views.delete_supplier, name='delete_supplier'),
# Inventory
path('inventory/add/', views.add_product, name='add_product'), path('inventory/add/', views.add_product, name='add_product'),
path('inventory/edit/<int:pk>/', views.edit_product, name='edit_product'),
path('inventory/delete/<int:pk>/', views.delete_product, name='delete_product'),
# Categories
path('inventory/category/add/', views.add_category, name='add_category'),
path('inventory/category/edit/<int:pk>/', views.edit_category, name='edit_category'),
path('inventory/category/delete/<int:pk>/', views.delete_category, name='delete_category'),
# Units
path('inventory/unit/add/', views.add_unit, name='add_unit'),
path('inventory/unit/edit/<int:pk>/', views.edit_unit, name='edit_unit'),
path('inventory/unit/delete/<int:pk>/', views.delete_unit, name='delete_unit'),
] ]

View File

@ -3,11 +3,16 @@ from django.db.models import Sum, Count, F
from django.db.models.functions import TruncDate, TruncMonth from django.db.models.functions import TruncDate, TruncMonth
from django.http import JsonResponse from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from .models import Product, Sale, Category, Customer, Supplier, Purchase, SaleItem, SystemSetting from .models import (
Product, Sale, Category, Unit, Customer, Supplier,
Purchase, PurchaseItem, PurchasePayment,
SaleItem, SalePayment, SystemSetting
)
import json import json
from datetime import timedelta from datetime import timedelta
from django.utils import timezone from django.utils import timezone
from django.contrib import messages from django.contrib import messages
from django.utils.text import slugify
def index(request): def index(request):
""" """
@ -56,20 +61,27 @@ def index(request):
return render(request, 'core/index.html', context) return render(request, 'core/index.html', context)
def inventory(request): def inventory(request):
products = Product.objects.all().select_related('category') products = Product.objects.all().select_related('category', 'unit', 'supplier')
categories = Category.objects.all() categories = Category.objects.all()
context = {'products': products, 'categories': categories} units = Unit.objects.all()
suppliers = Supplier.objects.all()
context = {
'products': products,
'categories': categories,
'units': units,
'suppliers': suppliers
}
return render(request, 'core/inventory.html', context) return render(request, 'core/inventory.html', context)
def pos(request): def pos(request):
products = Product.objects.all().filter(stock_quantity__gt=0) products = Product.objects.all().filter(stock_quantity__gt=0, is_active=True)
customers = Customer.objects.all() customers = Customer.objects.all()
categories = Category.objects.all() categories = Category.objects.all()
context = {'products': products, 'customers': customers, 'categories': categories} context = {'products': products, 'customers': customers, 'categories': categories}
return render(request, 'core/pos.html', context) return render(request, 'core/pos.html', context)
def customers(request): def customers(request):
customers_list = Customer.objects.all().annotate(total_sales=Sum('sale__total_amount')) customers_list = Customer.objects.all().annotate(total_sales=Sum('sales__total_amount'))
context = {'customers': customers_list} context = {'customers': customers_list}
return render(request, 'core/customers.html', context) return render(request, 'core/customers.html', context)
@ -78,12 +90,266 @@ def suppliers(request):
context = {'suppliers': suppliers_list} context = {'suppliers': suppliers_list}
return render(request, 'core/suppliers.html', context) return render(request, 'core/suppliers.html', context)
# --- Purchase Views ---
def purchases(request): def purchases(request):
purchases_list = Purchase.objects.all().select_related('supplier') purchases_list = Purchase.objects.all().select_related('supplier').order_by('-created_at')
suppliers_list = Supplier.objects.all() suppliers_list = Supplier.objects.all()
context = {'purchases': purchases_list, 'suppliers': suppliers_list} context = {'purchases': purchases_list, 'suppliers': suppliers_list}
return render(request, 'core/purchases.html', context) return render(request, 'core/purchases.html', context)
def purchase_create(request):
products = Product.objects.filter(is_active=True)
suppliers = Supplier.objects.all()
return render(request, 'core/purchase_create.html', {'products': products, 'suppliers': suppliers})
def purchase_detail(request, pk):
purchase = get_object_or_404(Purchase, pk=pk)
settings = SystemSetting.objects.first()
return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': settings})
@csrf_exempt
def create_purchase_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
supplier_id = data.get('supplier_id')
invoice_number = data.get('invoice_number', '')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
paid_amount = data.get('paid_amount', 0)
payment_type = data.get('payment_type', 'cash')
due_date = data.get('due_date')
notes = data.get('notes', '')
supplier = None
if supplier_id:
supplier = Supplier.objects.get(id=supplier_id)
purchase = Purchase.objects.create(
supplier=supplier,
invoice_number=invoice_number,
total_amount=total_amount,
paid_amount=paid_amount,
balance_due=float(total_amount) - float(paid_amount),
payment_type=payment_type,
due_date=due_date if due_date else None,
notes=notes
)
# Set status based on payments
if float(paid_amount) >= float(total_amount):
purchase.status = 'paid'
elif float(paid_amount) > 0:
purchase.status = 'partial'
else:
purchase.status = 'unpaid'
purchase.save()
# Record the initial payment if any
if float(paid_amount) > 0:
PurchasePayment.objects.create(
purchase=purchase,
amount=paid_amount,
payment_method=payment_type.capitalize(),
notes=_("Initial payment")
)
for item in items:
product = Product.objects.get(id=item['id'])
PurchaseItem.objects.create(
purchase=purchase,
product=product,
quantity=item['quantity'],
cost_price=item['price'],
line_total=item['line_total']
)
# Update Stock
product.stock_quantity += int(item['quantity'])
product.cost_price = item['price']
product.save()
return JsonResponse({'success': True, 'purchase_id': purchase.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
def add_purchase_payment(request, pk):
purchase = get_object_or_404(Purchase, pk=pk)
if request.method == 'POST':
amount = request.POST.get('amount')
payment_date = request.POST.get('payment_date', timezone.now().date())
payment_method = request.POST.get('payment_method', 'Cash')
notes = request.POST.get('notes', '')
PurchasePayment.objects.create(
purchase=purchase,
amount=amount,
payment_date=payment_date,
payment_method=payment_method,
notes=notes
)
purchase.update_balance()
messages.success(request, _("Payment added successfully!"))
return redirect('purchases')
def delete_purchase(request, pk):
purchase = get_object_or_404(Purchase, pk=pk)
for item in purchase.items.all():
item.product.stock_quantity -= item.quantity
item.product.save()
purchase.delete()
messages.success(request, _("Purchase deleted successfully!"))
return redirect('purchases')
# --- Sale Views ---
def invoice_list(request):
sales = Sale.objects.all().order_by('-created_at')
customers = Customer.objects.all()
return render(request, 'core/invoices.html', {'sales': sales, 'customers': customers})
def invoice_create(request):
products = Product.objects.filter(is_active=True)
customers = Customer.objects.all()
return render(request, 'core/invoice_create.html', {'products': products, 'customers': customers})
def invoice_detail(request, pk):
sale = get_object_or_404(Sale, pk=pk)
settings = SystemSetting.objects.first()
return render(request, 'core/invoice_detail.html', {'sale': sale, 'settings': settings})
@csrf_exempt
def create_sale_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
customer_id = data.get('customer_id')
invoice_number = data.get('invoice_number', '')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
paid_amount = data.get('paid_amount', 0)
discount = data.get('discount', 0)
payment_type = data.get('payment_type', 'cash')
due_date = data.get('due_date')
notes = data.get('notes', '')
customer = None
if customer_id:
customer = Customer.objects.get(id=customer_id)
sale = Sale.objects.create(
customer=customer,
invoice_number=invoice_number,
total_amount=total_amount,
paid_amount=paid_amount,
balance_due=float(total_amount) - float(paid_amount),
discount=discount,
payment_type=payment_type,
due_date=due_date if due_date else None,
notes=notes
)
# Set status based on payments
if float(paid_amount) >= float(total_amount):
sale.status = 'paid'
elif float(paid_amount) > 0:
sale.status = 'partial'
else:
sale.status = 'unpaid'
sale.save()
# Record initial payment if any
if float(paid_amount) > 0:
SalePayment.objects.create(
sale=sale,
amount=paid_amount,
payment_method=payment_type.capitalize(),
notes=_("Initial payment")
)
for item in items:
product = Product.objects.get(id=item['id'])
SaleItem.objects.create(
sale=sale,
product=product,
quantity=item['quantity'],
unit_price=item['price'],
line_total=item['line_total']
)
product.stock_quantity -= int(item['quantity'])
product.save()
settings = SystemSetting.objects.first()
if not settings:
settings = SystemSetting.objects.create()
return JsonResponse({
'success': True,
'sale_id': sale.id,
'business': {
'name': settings.business_name,
'address': settings.address,
'phone': settings.phone,
'email': settings.email,
'currency': settings.currency_symbol,
'vat_number': settings.vat_number,
'registration_number': settings.registration_number,
'logo_url': settings.logo.url if settings.logo else None
},
'sale': {
'id': sale.id,
'invoice_number': sale.invoice_number,
'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"),
'total': float(sale.total_amount),
'paid': float(sale.paid_amount),
'balance': float(sale.balance_due),
'items': [
{
'name_en': si.product.name_en,
'name_ar': si.product.name_ar,
'qty': si.quantity,
'price': float(si.unit_price),
'total': float(si.line_total)
} for si in sale.items.all()
]
}
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
def add_sale_payment(request, pk):
sale = get_object_or_404(Sale, pk=pk)
if request.method == 'POST':
amount = request.POST.get('amount')
payment_date = request.POST.get('payment_date', timezone.now().date())
payment_method = request.POST.get('payment_method', 'Cash')
notes = request.POST.get('notes', '')
SalePayment.objects.create(
sale=sale,
amount=amount,
payment_date=payment_date,
payment_method=payment_method,
notes=notes
)
sale.update_balance()
messages.success(request, _("Payment added successfully!"))
return redirect('invoices')
def delete_sale(request, pk):
sale = get_object_or_404(Sale, pk=pk)
for item in sale.items.all():
item.product.stock_quantity += item.quantity
item.product.save()
sale.delete()
messages.success(request, _("Sale deleted successfully!"))
return redirect('invoices')
# --- Other Management Views ---
def reports(request): def reports(request):
""" """
Smart Reports View Smart Reports View
@ -120,73 +386,18 @@ def settings_view(request):
settings.email = request.POST.get('email') settings.email = request.POST.get('email')
settings.currency_symbol = request.POST.get('currency_symbol') settings.currency_symbol = request.POST.get('currency_symbol')
settings.tax_rate = request.POST.get('tax_rate') settings.tax_rate = request.POST.get('tax_rate')
settings.vat_number = request.POST.get('vat_number')
settings.registration_number = request.POST.get('registration_number')
if 'logo' in request.FILES:
settings.logo = request.FILES['logo']
settings.save() settings.save()
messages.success(request, "Settings updated successfully!") messages.success(request, "Settings updated successfully!")
return redirect('settings') return redirect('settings')
return render(request, 'core/settings.html', {'settings': settings}) return render(request, 'core/settings.html', {'settings': settings})
@csrf_exempt
def create_sale_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
customer_id = data.get('customer_id')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
discount = data.get('discount', 0)
customer = None
if customer_id:
customer = Customer.objects.get(id=customer_id)
sale = Sale.objects.create(
customer=customer,
total_amount=total_amount,
discount=discount
)
for item in items:
product = Product.objects.get(id=item['id'])
SaleItem.objects.create(
sale=sale,
product=product,
quantity=item['quantity'],
unit_price=item['price'],
line_total=item['line_total']
)
product.stock_quantity -= item['quantity']
product.save()
settings = SystemSetting.objects.first()
return JsonResponse({
'success': True,
'sale_id': sale.id,
'business': {
'name': settings.business_name,
'address': settings.address,
'phone': settings.phone,
'currency': settings.currency_symbol
},
'sale': {
'id': sale.id,
'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"),
'total': float(sale.total_amount),
'items': [
{
'name_en': item.product.name_en,
'name_ar': item.product.name_ar,
'qty': item.quantity,
'price': float(item.unit_price),
'total': float(item.line_total)
} for item in sale.items.all()
]
}
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
def add_customer(request): def add_customer(request):
if request.method == 'POST': if request.method == 'POST':
name = request.POST.get('name') name = request.POST.get('name')
@ -197,6 +408,23 @@ def add_customer(request):
messages.success(request, "Customer added successfully!") messages.success(request, "Customer added successfully!")
return redirect('customers') return redirect('customers')
def edit_customer(request, pk):
customer = get_object_or_404(Customer, pk=pk)
if request.method == 'POST':
customer.name = request.POST.get('name')
customer.phone = request.POST.get('phone')
customer.email = request.POST.get('email')
customer.address = request.POST.get('address')
customer.save()
messages.success(request, "Customer updated successfully!")
return redirect('customers')
def delete_customer(request, pk):
customer = get_object_or_404(Customer, pk=pk)
customer.delete()
messages.success(request, "Customer deleted successfully!")
return redirect('customers')
def add_supplier(request): def add_supplier(request):
if request.method == 'POST': if request.method == 'POST':
name = request.POST.get('name') name = request.POST.get('name')
@ -206,31 +434,144 @@ def add_supplier(request):
messages.success(request, "Supplier added successfully!") messages.success(request, "Supplier added successfully!")
return redirect('suppliers') return redirect('suppliers')
def add_purchase(request): def edit_supplier(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
if request.method == 'POST': if request.method == 'POST':
supplier_id = request.POST.get('supplier') supplier.name = request.POST.get('name')
total_amount = request.POST.get('total_amount') supplier.contact_person = request.POST.get('contact_person')
supplier = get_object_or_404(Supplier, id=supplier_id) supplier.phone = request.POST.get('phone')
Purchase.objects.create(supplier=supplier, total_amount=total_amount) supplier.save()
messages.success(request, "Purchase recorded successfully!") messages.success(request, "Supplier updated successfully!")
return redirect('purchases') return redirect('suppliers')
def delete_supplier(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
supplier.delete()
messages.success(request, "Supplier deleted successfully!")
return redirect('suppliers')
def add_product(request): def add_product(request):
if request.method == 'POST': if request.method == 'POST':
name_en = request.POST.get('name_en') name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar') name_ar = request.POST.get('name_ar')
category_id = request.POST.get('category') category_id = request.POST.get('category')
unit_id = request.POST.get('unit')
supplier_id = request.POST.get('supplier')
sku = request.POST.get('sku') sku = request.POST.get('sku')
price = request.POST.get('price') cost_price = request.POST.get('cost_price', 0)
stock = request.POST.get('stock') price = request.POST.get('price', 0)
vat = request.POST.get('vat', 0)
opening_stock = request.POST.get('opening_stock', 0)
stock_quantity = request.POST.get('stock_quantity', 0)
is_active = request.POST.get('is_active') == 'on'
category = get_object_or_404(Category, id=category_id) category = get_object_or_404(Category, id=category_id)
Product.objects.create( unit = get_object_or_404(Unit, id=unit_id) if unit_id else None
supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None
product = Product.objects.create(
name_en=name_en, name_en=name_en,
name_ar=name_ar, name_ar=name_ar,
category=category, category=category,
unit=unit,
supplier=supplier,
sku=sku, sku=sku,
cost_price=cost_price,
price=price, price=price,
stock_quantity=stock vat=vat,
opening_stock=opening_stock,
stock_quantity=stock_quantity,
is_active=is_active
) )
if 'image' in request.FILES:
product.image = request.FILES['image']
product.save()
messages.success(request, "Product added successfully!") messages.success(request, "Product added successfully!")
return redirect('inventory') return redirect('inventory')
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.opening_stock = request.POST.get('opening_stock', 0)
product.stock_quantity = request.POST.get('stock_quantity', 0)
product.is_active = request.POST.get('is_active') == 'on'
if 'image' in request.FILES:
product.image = request.FILES['image']
product.save()
messages.success(request, "Product updated successfully!")
return redirect('inventory')
return redirect('inventory')
def delete_product(request, pk):
product = get_object_or_404(Product, pk=pk)
product.delete()
messages.success(request, "Product deleted successfully!")
return redirect('inventory')
def add_category(request):
if request.method == 'POST':
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar')
slug = slugify(name_en)
Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug)
messages.success(request, "Category added successfully!")
return redirect('inventory')
def edit_category(request, pk):
category = get_object_or_404(Category, pk=pk)
if request.method == 'POST':
category.name_en = request.POST.get('name_en')
category.name_ar = request.POST.get('name_ar')
category.slug = slugify(category.name_en)
category.save()
messages.success(request, "Category updated successfully!")
return redirect('inventory')
def delete_category(request, pk):
category = get_object_or_404(Category, pk=pk)
category.delete()
messages.success(request, "Category deleted successfully!")
return redirect('inventory')
def add_unit(request):
if request.method == 'POST':
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar')
short_name = request.POST.get('short_name')
Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name)
messages.success(request, "Unit added successfully!")
return redirect('inventory')
def edit_unit(request, pk):
unit = get_object_or_404(Unit, pk=pk)
if request.method == 'POST':
unit.name_en = request.POST.get('name_en')
unit.name_ar = request.POST.get('name_ar')
unit.short_name = request.POST.get('short_name')
unit.save()
messages.success(request, "Unit updated successfully!")
return redirect('inventory')
def delete_unit(request, pk):
unit = get_object_or_404(Unit, pk=pk)
unit.delete()
messages.success(request, "Unit deleted successfully!")
return redirect('inventory')

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB