Autosave: 20260202-093633
This commit is contained in:
parent
7d1c8df2b2
commit
5391ba1010
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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')
|
||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
28
core/migrations/0004_unit_product_unit.py
Normal file
28
core/migrations/0004_unit_product_unit.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 08:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_remove_systemsetting_logo_url_systemsetting_logo_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Unit',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name_en', models.CharField(max_length=50, verbose_name='Name (English)')),
|
||||||
|
('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')),
|
||||||
|
('short_name', models.CharField(max_length=10, verbose_name='Short Name')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='unit',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.unit'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 08:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_unit_product_unit'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='cost_price',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='Cost Price'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Active'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='opening_stock',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='Opening Stock'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='supplier',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.supplier'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='vat',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='VAT (%)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='image',
|
||||||
|
field=models.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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 08:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_product_cost_price_product_is_active_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='balance_due',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Balance Due'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='due_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Due Date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='invoice_number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Invoice Number'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='paid_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Paid Amount'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='payment_type',
|
||||||
|
field=models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit'), ('partial', 'Partial')], default='cash', max_length=20, verbose_name='Payment Type'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('paid', 'Paid'), ('partial', 'Partial'), ('unpaid', 'Unpaid')], default='paid', max_length=20, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='supplier',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchases', to='core.supplier'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchase',
|
||||||
|
name='total_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchaseItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
|
||||||
|
('cost_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Cost Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('purchase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.purchase')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchasePayment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')),
|
||||||
|
('payment_date', models.DateField(default=django.utils.timezone.now, verbose_name='Payment Date')),
|
||||||
|
('payment_method', models.CharField(default='Cash', max_length=50, verbose_name='Payment Method')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('purchase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='core.purchase')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 09:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0006_purchase_balance_due_purchase_due_date_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='balance_due',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Balance Due'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='due_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Due Date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='invoice_number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Invoice Number'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Notes'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='paid_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Paid Amount'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='payment_type',
|
||||||
|
field=models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit'), ('partial', 'Partial')], default='cash', max_length=20, verbose_name='Payment Type'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('paid', 'Paid'), ('partial', 'Partial'), ('unpaid', 'Unpaid')], default='paid', max_length=20, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sale',
|
||||||
|
name='customer',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to='core.customer'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sale',
|
||||||
|
name='discount',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Discount'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sale',
|
||||||
|
name='total_amount',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='line_total',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.PositiveIntegerField(verbose_name='Quantity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='saleitem',
|
||||||
|
name='unit_price',
|
||||||
|
field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SalePayment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')),
|
||||||
|
('payment_date', models.DateField(default=django.utils.timezone.now, verbose_name='Payment Date')),
|
||||||
|
('payment_method', models.CharField(default='Cash', max_length=50, verbose_name='Payment Method')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='core.sale')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
143
core/models.py
143
core/models.py
@ -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
|
||||||
|
|||||||
@ -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" %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
287
core/templates/core/invoice_create.html
Normal file
287
core/templates/core/invoice_create.html
Normal 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 %}
|
||||||
195
core/templates/core/invoice_detail.html
Normal file
195
core/templates/core/invoice_detail.html
Normal 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 %}
|
||||||
160
core/templates/core/invoices.html
Normal file
160
core/templates/core/invoices.html
Normal 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 %}
|
||||||
@ -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 %}
|
||||||
275
core/templates/core/purchase_create.html
Normal file
275
core/templates/core/purchase_create.html
Normal 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 %}
|
||||||
182
core/templates/core/purchase_detail.html
Normal file
182
core/templates/core/purchase_detail.html
Normal 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 %}
|
||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
|
|||||||
37
core/urls.py
37
core/urls.py
@ -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'),
|
||||||
]
|
]
|
||||||
497
core/views.py
497
core/views.py
@ -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')
|
||||||
|
|||||||
BIN
media/business_logos/albidar-logo.jpg
Normal file
BIN
media/business_logos/albidar-logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Loading…
x
Reference in New Issue
Block a user