improving the system
This commit is contained in:
parent
5391ba1010
commit
f80934e391
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 09:49
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Quotation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quotation_number', models.CharField(blank=True, max_length=50, verbose_name='Quotation Number')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
|
||||||
|
('discount', models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Discount')),
|
||||||
|
('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('converted', 'Converted to Invoice')], default='draft', max_length=20, verbose_name='Status')),
|
||||||
|
('valid_until', models.DateField(blank=True, null=True, verbose_name='Valid Until')),
|
||||||
|
('terms_and_conditions', models.TextField(blank=True, verbose_name='Terms and Conditions')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotations', to='core.customer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='quotation',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted_sale', to='core.quotation'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuotationItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
|
||||||
|
('unit_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('quotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.quotation')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-02 10:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0008_quotation_sale_quotation_quotationitem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchaseReturn',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('return_number', models.CharField(blank=True, max_length=50, verbose_name='Return Number')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('purchase', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='returns', to='core.purchase')),
|
||||||
|
('supplier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_returns', to='core.supplier')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PurchaseReturnItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
|
||||||
|
('cost_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Cost Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('purchase_return', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.purchasereturn')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SaleReturn',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('return_number', models.CharField(blank=True, max_length=50, verbose_name='Return Number')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_returns', to='core.customer')),
|
||||||
|
('sale', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='returns', to='core.sale')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SaleReturnItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
|
||||||
|
('unit_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price')),
|
||||||
|
('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
('sale_return', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.salereturn')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
@ -71,6 +71,7 @@ class Sale(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales")
|
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales")
|
||||||
|
quotation = models.ForeignKey('Quotation', on_delete=models.SET_NULL, null=True, blank=True, related_name="converted_sale")
|
||||||
invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True)
|
invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True)
|
||||||
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
|
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)
|
paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0)
|
||||||
@ -117,6 +118,38 @@ class SalePayment(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Payment of {self.amount} for Sale #{self.sale.id}"
|
return f"Payment of {self.amount} for Sale #{self.sale.id}"
|
||||||
|
|
||||||
|
class Quotation(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('draft', _('Draft')),
|
||||||
|
('sent', _('Sent')),
|
||||||
|
('accepted', _('Accepted')),
|
||||||
|
('rejected', _('Rejected')),
|
||||||
|
('converted', _('Converted to Invoice')),
|
||||||
|
]
|
||||||
|
|
||||||
|
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="quotations")
|
||||||
|
quotation_number = models.CharField(_("Quotation Number"), max_length=50, blank=True)
|
||||||
|
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
|
||||||
|
discount = models.DecimalField(_("Discount"), max_digits=15, decimal_places=3, default=0)
|
||||||
|
status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='draft')
|
||||||
|
valid_until = models.DateField(_("Valid Until"), null=True, blank=True)
|
||||||
|
terms_and_conditions = models.TextField(_("Terms and Conditions"), blank=True)
|
||||||
|
notes = models.TextField(_("Notes"), blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Quotation #{self.id} - {self.customer.name if self.customer else 'Guest'}"
|
||||||
|
|
||||||
|
class QuotationItem(models.Model):
|
||||||
|
quotation = models.ForeignKey(Quotation, on_delete=models.CASCADE, related_name="items")
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
|
quantity = models.PositiveIntegerField(_("Quantity"))
|
||||||
|
unit_price = models.DecimalField(_("Unit 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 Purchase(models.Model):
|
class Purchase(models.Model):
|
||||||
PAYMENT_TYPE_CHOICES = [
|
PAYMENT_TYPE_CHOICES = [
|
||||||
('cash', _('Cash')),
|
('cash', _('Cash')),
|
||||||
@ -175,6 +208,48 @@ class PurchasePayment(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Payment of {self.amount} for Purchase #{self.purchase.id}"
|
return f"Payment of {self.amount} for Purchase #{self.purchase.id}"
|
||||||
|
|
||||||
|
class SaleReturn(models.Model):
|
||||||
|
sale = models.ForeignKey(Sale, on_delete=models.SET_NULL, null=True, blank=True, related_name="returns")
|
||||||
|
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_returns")
|
||||||
|
return_number = models.CharField(_("Return Number"), max_length=50, blank=True)
|
||||||
|
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
|
||||||
|
notes = models.TextField(_("Notes"), blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Sale Return #{self.id} - {self.customer.name if self.customer else 'Guest'}"
|
||||||
|
|
||||||
|
class SaleReturnItem(models.Model):
|
||||||
|
sale_return = models.ForeignKey(SaleReturn, on_delete=models.CASCADE, related_name="items")
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
|
quantity = models.PositiveIntegerField(_("Quantity"))
|
||||||
|
unit_price = models.DecimalField(_("Unit 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 PurchaseReturn(models.Model):
|
||||||
|
purchase = models.ForeignKey(Purchase, on_delete=models.SET_NULL, null=True, blank=True, related_name="returns")
|
||||||
|
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_returns")
|
||||||
|
return_number = models.CharField(_("Return Number"), max_length=50, blank=True)
|
||||||
|
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
|
||||||
|
notes = models.TextField(_("Notes"), blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Purchase Return #{self.id} - {self.supplier.name if self.supplier else 'N/A'}"
|
||||||
|
|
||||||
|
class PurchaseReturnItem(models.Model):
|
||||||
|
purchase_return = models.ForeignKey(PurchaseReturn, 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 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)
|
||||||
|
|||||||
@ -47,19 +47,31 @@
|
|||||||
<i class="bi bi-speedometer2"></i> {% trans "Dashboard" %}
|
<i class="bi bi-speedometer2"></i> {% trans "Dashboard" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="mt-3 px-4 small text-muted text-uppercase fw-bold">{% trans "Sales" %}</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'pos' %}" class="{% if request.resolver_match.url_name == 'pos' %}active{% endif %}">
|
<a href="{% url 'pos' %}" class="{% if request.resolver_match.url_name == 'pos' %}active{% endif %}">
|
||||||
<i class="bi bi-shop"></i> {% trans "POS System" %}
|
<i class="bi bi-shop"></i> {% trans "POS System" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'invoices' %}" class="{% if request.resolver_match.url_name == 'invoices' %}active{% endif %}">
|
<a href="{% url 'invoice_create' %}" class="{% if request.resolver_match.url_name == 'invoice_create' %}active{% endif %}">
|
||||||
<i class="bi bi-file-earmark-text"></i> {% trans "Invoices" %}
|
<i class="bi bi-plus-circle"></i> {% trans "New Sales" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'reports' %}" class="{% if request.resolver_match.url_name == 'reports' %}active{% endif %}">
|
<a href="{% url 'invoices' %}" class="{% if request.resolver_match.url_name == 'invoices' or request.resolver_match.url_name == 'invoice_detail' %}active{% endif %}">
|
||||||
<i class="bi bi-graph-up-arrow"></i> {% trans "Reports" %}
|
<i class="bi bi-file-earmark-text"></i> {% trans "Sales Invoices" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'quotations' %}" class="{% if request.resolver_match.url_name == 'quotations' or request.resolver_match.url_name == 'quotation_create' or request.resolver_match.url_name == 'quotation_detail' %}active{% endif %}">
|
||||||
|
<i class="bi bi-file-earmark-spreadsheet"></i> {% trans "Quotation" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'sales_returns' %}" class="{% if 'sales/returns' in request.path %}active{% endif %}">
|
||||||
|
<i class="bi bi-arrow-return-left"></i> {% trans "Sales Return" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@ -70,10 +82,25 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'purchases' %}" class="{% if request.resolver_match.url_name == 'purchases' %}active{% endif %}">
|
<a href="{% url 'purchases' %}" class="{% if request.resolver_match.url_name == 'purchases' or request.resolver_match.url_name == 'purchase_create' or request.resolver_match.url_name == 'purchase_detail' %}active{% endif %}">
|
||||||
<i class="bi bi-cart-check"></i> {% trans "Purchases" %}
|
<i class="bi bi-cart-check"></i> {% trans "Purchases" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'purchase_returns' %}" class="{% if 'purchases/returns' in request.path %}active{% endif %}">
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'barcode_labels' %}" class="{% if request.resolver_match.url_name == 'barcode_labels' %}active{% endif %}">
|
||||||
|
<i class="bi bi-upc-scan"></i> {% trans "Barcode Printing" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<i class="bi bi-arrow-return-right"></i> {% trans "Purchase Return" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'reports' %}" class="{% if request.resolver_match.url_name == 'reports' %}active{% endif %}">
|
||||||
|
<i class="bi bi-graph-up-arrow"></i> {% trans "Reports" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="mt-3 px-4 small text-muted text-uppercase fw-bold">{% trans "Contacts" %}</li>
|
<li class="mt-3 px-4 small text-muted text-uppercase fw-bold">{% trans "Contacts" %}</li>
|
||||||
<li>
|
<li>
|
||||||
@ -93,6 +120,11 @@
|
|||||||
<i class="bi bi-gear"></i> {% trans "Settings" %}
|
<i class="bi bi-gear"></i> {% trans "Settings" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/">
|
||||||
|
<i class="bi bi-shield-lock"></i> {% trans "Django Admin" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="mt-auto p-4 border-top">
|
<div class="mt-auto p-4 border-top">
|
||||||
|
|||||||
429
core/templates/core/barcode_labels.html
Normal file
429
core/templates/core/barcode_labels.html
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid mt-4 mb-5 no-print">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="h4 mb-0"><i class="bi bi-upc-scan me-2"></i>Barcode Label Printing</h2>
|
||||||
|
<button onclick="window.print()" class="btn btn-primary">
|
||||||
|
<i class="bi bi-printer me-2"></i>Print Labels
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Product Selection -->
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="card shadow-sm border-0 mb-4">
|
||||||
|
<div class="card-header bg-white py-3">
|
||||||
|
<h5 class="card-title mb-0">1. Select Products</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" id="productSearch" class="form-control border-start-0 ps-0" placeholder="Search by name or SKU...">
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive" style="max-height: 500px;">
|
||||||
|
<table class="table table-hover align-middle" id="productTable">
|
||||||
|
<thead class="table-light sticky-top">
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>SKU</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for product in products %}
|
||||||
|
<tr class="product-row" data-name="{{ product.name_en }}" data-sku="{{ product.sku }}">
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold">{{ product.name_en }}</div>
|
||||||
|
<small class="text-muted">{{ product.name_ar }}</small>
|
||||||
|
</td>
|
||||||
|
<td><code>{{ product.sku }}</code></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary add-to-queue"
|
||||||
|
data-id="{{ product.id }}"
|
||||||
|
data-name="{{ product.name_en }}"
|
||||||
|
data-sku="{{ product.sku }}"
|
||||||
|
data-price="{{ product.price }}">
|
||||||
|
<i class="bi bi-plus"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Label Queue & Settings -->
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="card shadow-sm border-0 mb-4">
|
||||||
|
<div class="card-header bg-white py-3">
|
||||||
|
<h5 class="card-title mb-0">2. Label Queue & Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Label Size / Type</label>
|
||||||
|
<select id="labelType" class="form-select">
|
||||||
|
<option value="standard">Standard Sticker (50mm x 25mm)</option>
|
||||||
|
<option value="small">Small Sticker (38mm x 25mm)</option>
|
||||||
|
<option value="a4-24">A4 Sheet (3x8 = 24 labels)</option>
|
||||||
|
<option value="a4-40">A4 Sheet (4x10 = 40 labels)</option>
|
||||||
|
<option value="price-tag">Jewelry / Price Tag (Small)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Include Fields</label>
|
||||||
|
<div class="d-flex gap-3 mt-2">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="showName" checked>
|
||||||
|
<label class="form-check-label" for="showName">Name</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="showPrice" checked>
|
||||||
|
<label class="form-check-label" for="showPrice">Price</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="showSKU" checked>
|
||||||
|
<label class="form-check-label" for="showSKU">SKU Text</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th style="width: 120px;">Qty of Labels</th>
|
||||||
|
<th style="width: 50px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="labelQueue">
|
||||||
|
<!-- Dynamic content -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div id="emptyQueue" class="text-center py-4 text-muted">
|
||||||
|
<i class="bi bi-cart-x display-4 d-block mb-2"></i>
|
||||||
|
Queue is empty. Select products to start.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Live Preview -->
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-header bg-white py-3">
|
||||||
|
<h5 class="card-title mb-0">3. Live Preview (Single Label)</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center bg-light p-5">
|
||||||
|
<div id="previewContainer" class="d-inline-block bg-white shadow-sm p-3 border">
|
||||||
|
<div id="previewLabelContent">
|
||||||
|
<div class="preview-name fw-bold small mb-1">Product Name</div>
|
||||||
|
<svg id="previewBarcode"></svg>
|
||||||
|
<div class="preview-footer d-flex justify-content-between mt-1 small">
|
||||||
|
<span class="preview-sku">SKU12345</span>
|
||||||
|
<span class="preview-price fw-bold">OMR 0.000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-muted small">
|
||||||
|
Note: This is a preview of the layout. Actual print layout depends on settings above.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PRINT VIEW (Hidden by default) -->
|
||||||
|
<div id="printArea" class="print-only">
|
||||||
|
<!-- Generated labels will go here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Print Styles */
|
||||||
|
@media screen {
|
||||||
|
.print-only { display: none; }
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
.print-only { display: block; }
|
||||||
|
body { margin: 0; padding: 0; background: white; }
|
||||||
|
@page { margin: 0; }
|
||||||
|
|
||||||
|
.label-sheet {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 5mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standard Sticker (50x25) */
|
||||||
|
.label-standard {
|
||||||
|
width: 50mm;
|
||||||
|
height: 25mm;
|
||||||
|
border: 0.1mm solid #eee; /* Light border for cutting/reference */
|
||||||
|
margin: 1mm;
|
||||||
|
padding: 2mm;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Sticker (38x25) */
|
||||||
|
.label-small {
|
||||||
|
width: 38mm;
|
||||||
|
height: 25mm;
|
||||||
|
border: 0.1mm solid #eee;
|
||||||
|
margin: 1mm;
|
||||||
|
padding: 1.5mm;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A4 24 Labels (3x8) */
|
||||||
|
.label-a4-24 {
|
||||||
|
width: 63.5mm;
|
||||||
|
height: 33.9mm;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2mm;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A4 40 Labels (4x10) */
|
||||||
|
.label-a4-40 {
|
||||||
|
width: 48.5mm;
|
||||||
|
height: 25.4mm;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1mm;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price Tag */
|
||||||
|
.label-price-tag {
|
||||||
|
width: 30mm;
|
||||||
|
height: 15mm;
|
||||||
|
margin: 1mm;
|
||||||
|
padding: 1mm;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-item svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 8pt;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.label-price {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewBarcode {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const queue = [];
|
||||||
|
const labelQueueBody = document.getElementById('labelQueue');
|
||||||
|
const emptyQueue = document.getElementById('emptyQueue');
|
||||||
|
const printArea = document.getElementById('printArea');
|
||||||
|
|
||||||
|
// Add to Queue
|
||||||
|
document.querySelectorAll('.add-to-queue').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const product = {
|
||||||
|
id: btn.dataset.id,
|
||||||
|
name: btn.dataset.name,
|
||||||
|
sku: btn.dataset.sku,
|
||||||
|
price: btn.dataset.price,
|
||||||
|
qty: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = queue.find(p => p.id === product.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.qty++;
|
||||||
|
} else {
|
||||||
|
queue.push(product);
|
||||||
|
}
|
||||||
|
renderQueue();
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderQueue() {
|
||||||
|
if (queue.length === 0) {
|
||||||
|
labelQueueBody.innerHTML = '';
|
||||||
|
emptyQueue.classList.remove('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emptyQueue.classList.add('d-none');
|
||||||
|
labelQueueBody.innerHTML = queue.map((p, index) => `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold small">${p.name}</div>
|
||||||
|
<code class="text-muted small">${p.sku}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm qty-input"
|
||||||
|
value="${p.qty}" min="1" onchange="updateQty(${index}, this.value)">
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-sm btn-link text-danger" onclick="removeFromQueue(${index})">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
preparePrint();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.updateQty = (index, val) => {
|
||||||
|
queue[index].qty = parseInt(val) || 1;
|
||||||
|
preparePrint();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeFromQueue = (index) => {
|
||||||
|
queue.splice(index, 1);
|
||||||
|
renderQueue();
|
||||||
|
updatePreview();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Product Search
|
||||||
|
document.getElementById('productSearch').addEventListener('input', function() {
|
||||||
|
const query = this.value.toLowerCase();
|
||||||
|
document.querySelectorAll('.product-row').forEach(row => {
|
||||||
|
const name = row.dataset.name.toLowerCase();
|
||||||
|
const sku = row.dataset.sku.toLowerCase();
|
||||||
|
if (name.includes(query) || sku.includes(query)) {
|
||||||
|
row.style.display = '';
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preview Logic
|
||||||
|
function updatePreview() {
|
||||||
|
if (queue.length === 0) return;
|
||||||
|
const lastProduct = queue[queue.length - 1];
|
||||||
|
|
||||||
|
document.querySelector('.preview-name').innerText = lastProduct.name;
|
||||||
|
document.querySelector('.preview-sku').innerText = lastProduct.sku;
|
||||||
|
document.querySelector('.preview-price').innerText = 'OMR ' + parseFloat(lastProduct.price).toFixed(3);
|
||||||
|
|
||||||
|
JsBarcode("#previewBarcode", lastProduct.sku, {
|
||||||
|
format: "CODE128",
|
||||||
|
width: 2,
|
||||||
|
height: 40,
|
||||||
|
displayValue: false,
|
||||||
|
margin: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply visibility toggles
|
||||||
|
document.querySelector('.preview-name').style.display = document.getElementById('showName').checked ? '' : 'none';
|
||||||
|
document.querySelector('.preview-price').style.display = document.getElementById('showPrice').checked ? '' : 'none';
|
||||||
|
document.querySelector('.preview-sku').style.display = document.getElementById('showSKU').checked ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare Print Area
|
||||||
|
function preparePrint() {
|
||||||
|
printArea.innerHTML = '';
|
||||||
|
const labelType = document.getElementById('labelType').value;
|
||||||
|
const showName = document.getElementById('showName').checked;
|
||||||
|
const showPrice = document.getElementById('showPrice').checked;
|
||||||
|
const showSKU = document.getElementById('showSKU').checked;
|
||||||
|
|
||||||
|
const sheet = document.createElement('div');
|
||||||
|
sheet.className = 'label-sheet';
|
||||||
|
|
||||||
|
queue.forEach(p => {
|
||||||
|
for (let i = 0; i < p.qty; i++) {
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = `label-item label-${labelType}`;
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
if (showName) content += `<div class="label-text">${p.name}</div>`;
|
||||||
|
|
||||||
|
const svgId = `barcode-${p.id}-${i}`;
|
||||||
|
content += `<svg id="${svgId}"></svg>`;
|
||||||
|
|
||||||
|
if (showSKU || showPrice) {
|
||||||
|
content += `<div class="label-text d-flex justify-content-between">`;
|
||||||
|
if (showSKU) content += `<span>${p.sku}</span>`;
|
||||||
|
if (showPrice) content += `<span class="label-price">OMR ${parseFloat(p.price).toFixed(3)}</span>`;
|
||||||
|
content += `</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.innerHTML = content;
|
||||||
|
sheet.appendChild(label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
printArea.appendChild(sheet);
|
||||||
|
|
||||||
|
// Generate barcodes for each SVG
|
||||||
|
queue.forEach(p => {
|
||||||
|
for (let i = 0; i < p.qty; i++) {
|
||||||
|
const svgId = `barcode-${p.id}-${i}`;
|
||||||
|
JsBarcode(`#${svgId}`, p.sku, {
|
||||||
|
format: "CODE128",
|
||||||
|
width: 1.5,
|
||||||
|
height: 35,
|
||||||
|
displayValue: false,
|
||||||
|
margin: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listeners for settings change
|
||||||
|
document.getElementById('labelType').addEventListener('change', preparePrint);
|
||||||
|
document.getElementById('showName').addEventListener('change', () => { updatePreview(); preparePrint(); });
|
||||||
|
document.getElementById('showPrice').addEventListener('change', () => { updatePreview(); preparePrint(); });
|
||||||
|
document.getElementById('showSKU').addEventListener('change', () => { updatePreview(); preparePrint(); });
|
||||||
|
|
||||||
|
// Initial Preview
|
||||||
|
updatePreview();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -8,15 +8,20 @@
|
|||||||
<!-- Action Bar -->
|
<!-- Action Bar -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
<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">
|
<a href="{% url 'invoices' %}" class="btn btn-light rounded-3">
|
||||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Invoices" %}
|
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Invoices" %} / العودة إلى الفواتير
|
||||||
</a>
|
</a>
|
||||||
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
|
<div class="d-flex gap-2">
|
||||||
<i class="bi bi-printer me-2"></i>{% trans "Print Invoice" %}
|
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
|
||||||
|
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
|
||||||
</button>
|
</button>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Invoice Content -->
|
<!-- Invoice Content -->
|
||||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
<div id="invoice-card" class="card border-0 shadow-sm rounded-4 overflow-hidden mx-auto" style="max-width: 800px;">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="p-5 bg-white">
|
<div class="p-5 bg-white">
|
||||||
@ -32,23 +37,23 @@
|
|||||||
<p class="mb-1"><i class="bi bi-telephone me-2"></i>{{ settings.phone }}</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>
|
<p class="mb-1"><i class="bi bi-envelope me-2"></i>{{ settings.email }}</p>
|
||||||
{% if settings.vat_number %}
|
{% if settings.vat_number %}
|
||||||
<p class="mb-0"><i class="bi bi-receipt me-2"></i>{% trans "VAT" %}: {{ settings.vat_number }}</p>
|
<p class="mb-0"><i class="bi bi-receipt me-2"></i>{% trans "VAT" %} / الضريبة: {{ settings.vat_number }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-sm-end">
|
<div class="col-sm-6 text-sm-end" dir="rtl">
|
||||||
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4">{% trans "Tax Invoice" %}</h1>
|
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4" dir="ltr">{% trans "Tax Invoice" %} / فاتورة ضريبية</h1>
|
||||||
<div class="mb-4">
|
<div class="mb-4 text-sm-end" dir="ltr">
|
||||||
<div class="fw-bold text-dark">{% trans "Invoice Number" %}</div>
|
<div class="fw-bold text-dark text-uppercase small">{% trans "Invoice Number" %} / رقم الفاتورة</div>
|
||||||
<div class="h5">{{ sale.invoice_number|default:sale.id }}</div>
|
<div class="h5">{{ sale.invoice_number|default:sale.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3 text-sm-end" dir="ltr">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="small text-muted fw-bold">{% trans "Issue Date" %}</div>
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Issue Date" %} / تاريخ الإصدار</div>
|
||||||
<div>{{ sale.created_at|date:"Y-m-d" }}</div>
|
<div>{{ sale.created_at|date:"Y-m-d" }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="small text-muted fw-bold">{% trans "Due Date" %}</div>
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Due Date" %} / تاريخ الاستحقاق</div>
|
||||||
<div>{{ sale.due_date|date:"Y-m-d"|default:"-" }}</div>
|
<div>{{ sale.due_date|date:"Y-m-d"|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -57,7 +62,7 @@
|
|||||||
|
|
||||||
<div class="row mb-5">
|
<div class="row mb-5">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Customer Information" %}</div>
|
<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>
|
<div class="h5 fw-bold mb-1">{{ sale.customer.name|default:_("Guest Customer") }}</div>
|
||||||
{% if sale.customer.phone %}
|
{% if sale.customer.phone %}
|
||||||
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ sale.customer.phone }}</div>
|
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ sale.customer.phone }}</div>
|
||||||
@ -67,14 +72,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-sm-end">
|
<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 class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Payment Status" %} / حالة الدفع</div>
|
||||||
<div>
|
<div>
|
||||||
{% if sale.status == 'paid' %}
|
{% if sale.status == 'paid' %}
|
||||||
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %}</span>
|
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %} / مدفوع بالكامل</span>
|
||||||
{% elif sale.status == 'partial' %}
|
{% elif sale.status == 'partial' %}
|
||||||
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %}</span>
|
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %} / مدفوع جزئياً</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %}</span>
|
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %} / غير مدفوع</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,10 +90,22 @@
|
|||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="py-3 ps-4 border-0">{% trans "Item Description" %}</th>
|
<th class="py-3 ps-4 border-0">
|
||||||
<th class="py-3 text-center border-0">{% trans "Unit Price" %}</th>
|
<div class="small text-muted">{% trans "Item Description" %}</div>
|
||||||
<th class="py-3 text-center border-0">{% trans "Quantity" %}</th>
|
<div class="small">وصف العنصر</div>
|
||||||
<th class="py-3 text-end pe-4 border-0">{% trans "Total" %}</th>
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Unit Price" %}</div>
|
||||||
|
<div class="small">سعر الوحدة</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Quantity" %}</div>
|
||||||
|
<div class="small">الكمية</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-end pe-4 border-0">
|
||||||
|
<div class="small text-muted">{% trans "Total" %}</div>
|
||||||
|
<div class="small">المجموع</div>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -107,29 +124,44 @@
|
|||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="border-0"></td>
|
<td colspan="2" class="border-0"></td>
|
||||||
<td class="text-center py-3 fw-bold border-top">{% trans "Subtotal" %}</td>
|
<td class="text-center py-3 fw-bold border-top">
|
||||||
|
<div>{% trans "Subtotal" %}</div>
|
||||||
|
<div class="small fw-normal">المجموع الفرعي</div>
|
||||||
|
</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>
|
<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>
|
</tr>
|
||||||
{% if sale.discount > 0 %}
|
{% if sale.discount > 0 %}
|
||||||
<tr class="text-muted">
|
<tr class="text-muted">
|
||||||
<td colspan="2" class="border-0"></td>
|
<td colspan="2" class="border-0"></td>
|
||||||
<td class="text-center py-2 fw-bold">{% trans "Discount" %}</td>
|
<td class="text-center py-2 fw-bold">
|
||||||
|
<div>{% trans "Discount" %}</div>
|
||||||
|
<div class="small fw-normal">الخصم</div>
|
||||||
|
</td>
|
||||||
<td class="text-end pe-4 py-2 fw-bold">-{{ settings.currency_symbol }}{{ sale.discount|floatformat:3 }}</td>
|
<td class="text-end pe-4 py-2 fw-bold">-{{ settings.currency_symbol }}{{ sale.discount|floatformat:3 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="border-0"></td>
|
<td colspan="2" class="border-0"></td>
|
||||||
<td class="text-center py-3 fw-bold">{% trans "Grand Total" %}</td>
|
<td class="text-center py-3 fw-bold">
|
||||||
|
<div>{% trans "Grand Total" %}</div>
|
||||||
|
<div class="small fw-normal">المجموع الكلي</div>
|
||||||
|
</td>
|
||||||
<td class="text-end pe-4 py-3 h5 fw-bold text-primary">{{ settings.currency_symbol }}{{ sale.total_amount|floatformat:3 }}</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>
|
||||||
<tr class="text-success">
|
<tr class="text-success">
|
||||||
<td colspan="2" class="border-0"></td>
|
<td colspan="2" class="border-0"></td>
|
||||||
<td class="text-center py-2 fw-bold">{% trans "Total Paid" %}</td>
|
<td class="text-center py-2 fw-bold">
|
||||||
|
<div>{% trans "Total Paid" %}</div>
|
||||||
|
<div class="small fw-normal">إجمالي المدفوع</div>
|
||||||
|
</td>
|
||||||
<td class="text-end pe-4 py-2 fw-bold">{{ settings.currency_symbol }}{{ sale.paid_amount|floatformat:3 }}</td>
|
<td class="text-end pe-4 py-2 fw-bold">{{ settings.currency_symbol }}{{ sale.paid_amount|floatformat:3 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="text-danger">
|
<tr class="text-danger">
|
||||||
<td colspan="2" class="border-0"></td>
|
<td colspan="2" class="border-0"></td>
|
||||||
<td class="text-center py-2 fw-bold border-top">{% trans "Balance Due" %}</td>
|
<td class="text-center py-2 fw-bold border-top">
|
||||||
|
<div>{% trans "Balance Due" %}</div>
|
||||||
|
<div class="small fw-normal">الرصيد المستحق</div>
|
||||||
|
</td>
|
||||||
<td class="text-end pe-4 py-2 h5 fw-bold border-top">{{ settings.currency_symbol }}{{ sale.balance_due|floatformat:3 }}</td>
|
<td class="text-end pe-4 py-2 h5 fw-bold border-top">{{ settings.currency_symbol }}{{ sale.balance_due|floatformat:3 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@ -139,15 +171,15 @@
|
|||||||
<!-- Payment History -->
|
<!-- Payment History -->
|
||||||
{% if sale.payments.exists %}
|
{% if sale.payments.exists %}
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment Records" %}</h5>
|
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment Records" %} / سجلات الدفع</h5>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-bordered">
|
<table class="table table-sm table-bordered">
|
||||||
<thead class="bg-light small">
|
<thead class="bg-light small">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Date" %}</th>
|
<th>{% trans "Date" %} / التاريخ</th>
|
||||||
<th>{% trans "Method" %}</th>
|
<th>{% trans "Method" %} / الطريقة</th>
|
||||||
<th>{% trans "Amount" %}</th>
|
<th>{% trans "Amount" %} / المبلغ</th>
|
||||||
<th>{% trans "Notes" %}</th>
|
<th>{% trans "Notes" %} / ملاحظات</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="small">
|
<tbody class="small">
|
||||||
@ -168,28 +200,65 @@
|
|||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
{% if sale.notes %}
|
{% if sale.notes %}
|
||||||
<div class="bg-light p-4 rounded-3 mb-5">
|
<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>
|
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Internal Notes" %} / ملاحظات داخلية</h6>
|
||||||
<p class="mb-0 small">{{ sale.notes }}</p>
|
<p class="mb-0 small">{{ sale.notes }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="text-center text-muted small mt-5 border-top pt-4">
|
<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-1">{% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!</p>
|
||||||
<p class="mb-0">{% trans "Software by Meezan" %}</p>
|
<p class="mb-0">{% trans "Software by Meezan" %} / برمجة ميزان</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function downloadPDF() {
|
||||||
|
const element = document.getElementById('invoice-card');
|
||||||
|
const opt = {
|
||||||
|
margin: 0,
|
||||||
|
filename: 'Invoice_{{ sale.invoice_number|default:sale.id }}.pdf',
|
||||||
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
|
html2canvas: { scale: 2, useCORS: true, letterRendering: true },
|
||||||
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||||
|
};
|
||||||
|
html2pdf().set(opt).from(element).save();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@media print {
|
@media print {
|
||||||
@page { size: portrait; margin: 0; }
|
@page {
|
||||||
body { background-color: white !important; }
|
size: A4 portrait;
|
||||||
.container { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; }
|
margin: 0;
|
||||||
.card { box-shadow: none !important; border: none !important; }
|
}
|
||||||
|
body {
|
||||||
|
background-color: white !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 210mm !important;
|
||||||
|
max-width: 210mm !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
#invoice-card {
|
||||||
|
width: 210mm !important;
|
||||||
|
min-height: 297mm !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
.d-print-none { display: none !important; }
|
.d-print-none { display: none !important; }
|
||||||
.p-5 { padding: 2rem !important; }
|
.p-5 { padding: 15mm !important; }
|
||||||
|
.table-responsive { overflow: visible !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -8,15 +8,20 @@
|
|||||||
<!-- Action Bar -->
|
<!-- Action Bar -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
<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">
|
<a href="{% url 'purchases' %}" class="btn btn-light rounded-3">
|
||||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to List" %}
|
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to List" %} / العودة للقائمة
|
||||||
</a>
|
</a>
|
||||||
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
|
<div class="d-flex gap-2">
|
||||||
<i class="bi bi-printer me-2"></i>{% trans "Print Invoice" %}
|
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
|
||||||
|
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
|
||||||
</button>
|
</button>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Invoice Content -->
|
<!-- Invoice Content -->
|
||||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
<div id="purchase-card" class="card border-0 shadow-sm rounded-4 overflow-hidden mx-auto" style="max-width: 800px;">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="p-5 bg-white">
|
<div class="p-5 bg-white">
|
||||||
@ -32,23 +37,23 @@
|
|||||||
<p class="mb-1"><i class="bi bi-telephone me-2"></i>{{ settings.phone }}</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>
|
<p class="mb-1"><i class="bi bi-envelope me-2"></i>{{ settings.email }}</p>
|
||||||
{% if settings.vat_number %}
|
{% if settings.vat_number %}
|
||||||
<p class="mb-0"><i class="bi bi-receipt me-2"></i>{% trans "VAT" %}: {{ settings.vat_number }}</p>
|
<p class="mb-0"><i class="bi bi-receipt me-2"></i>{% trans "VAT" %} / الضريبة: {{ settings.vat_number }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-sm-end">
|
<div class="col-sm-6 text-sm-end">
|
||||||
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4">{% trans "Purchase Invoice" %}</h1>
|
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4">{% trans "Purchase Invoice" %} / فاتورة مشتريات</h1>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="fw-bold text-dark">{% trans "Invoice Number" %}</div>
|
<div class="fw-bold text-dark text-uppercase small">{% trans "Invoice Number" %} / رقم الفاتورة</div>
|
||||||
<div class="h5">{{ purchase.invoice_number|default:purchase.id }}</div>
|
<div class="h5">{{ purchase.invoice_number|default:purchase.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="small text-muted fw-bold">{% trans "Issue Date" %}</div>
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Issue Date" %} / تاريخ الإصدار</div>
|
||||||
<div>{{ purchase.created_at|date:"Y-m-d" }}</div>
|
<div>{{ purchase.created_at|date:"Y-m-d" }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="small text-muted fw-bold">{% trans "Due Date" %}</div>
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Due Date" %} / تاريخ الاستحقاق</div>
|
||||||
<div>{{ purchase.due_date|date:"Y-m-d"|default:"-" }}</div>
|
<div>{{ purchase.due_date|date:"Y-m-d"|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -57,7 +62,7 @@
|
|||||||
|
|
||||||
<div class="row mb-5">
|
<div class="row mb-5">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Supplier Information" %}</div>
|
<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>
|
<div class="h5 fw-bold mb-1">{{ purchase.supplier.name }}</div>
|
||||||
{% if purchase.supplier.phone %}
|
{% if purchase.supplier.phone %}
|
||||||
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ purchase.supplier.phone }}</div>
|
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ purchase.supplier.phone }}</div>
|
||||||
@ -67,14 +72,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-sm-end">
|
<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 class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Payment Status" %} / حالة الدفع</div>
|
||||||
<div>
|
<div>
|
||||||
{% if purchase.status == 'paid' %}
|
{% if purchase.status == 'paid' %}
|
||||||
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %}</span>
|
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %} / مدفوع بالكامل</span>
|
||||||
{% elif purchase.status == 'partial' %}
|
{% elif purchase.status == 'partial' %}
|
||||||
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %}</span>
|
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %} / مدفوع جزئياً</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %}</span>
|
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %} / غير مدفوع</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,10 +90,22 @@
|
|||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="py-3 ps-4 border-0">{% trans "Item Description" %}</th>
|
<th class="py-3 ps-4 border-0">
|
||||||
<th class="py-3 text-center border-0">{% trans "Cost Price" %}</th>
|
<div class="small text-muted">{% trans "Item Description" %}</div>
|
||||||
<th class="py-3 text-center border-0">{% trans "Quantity" %}</th>
|
<div class="small">وصف العنصر</div>
|
||||||
<th class="py-3 text-end pe-4 border-0">{% trans "Total" %}</th>
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Cost Price" %}</div>
|
||||||
|
<div class="small">سعر التكلفة</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Quantity" %}</div>
|
||||||
|
<div class="small">الكمية</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-end pe-4 border-0">
|
||||||
|
<div class="small text-muted">{% trans "Total" %}</div>
|
||||||
|
<div class="small">المجموع</div>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -107,17 +124,26 @@
|
|||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="border-0"></td>
|
<td colspan="2" class="border-0"></td>
|
||||||
<td class="text-center py-3 fw-bold border-top">{% trans "Grand Total" %}</td>
|
<td class="text-center py-3 fw-bold border-top">
|
||||||
|
<div>{% trans "Grand Total" %}</div>
|
||||||
|
<div class="small fw-normal">المجموع الكلي</div>
|
||||||
|
</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>
|
<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>
|
||||||
<tr class="text-success">
|
<tr class="text-success">
|
||||||
<td colspan="2" class="border-0"></td>
|
<td colspan="2" class="border-0"></td>
|
||||||
<td class="text-center py-2 fw-bold">{% trans "Total Paid" %}</td>
|
<td class="text-center py-2 fw-bold">
|
||||||
|
<div>{% trans "Total Paid" %}</div>
|
||||||
|
<div class="small fw-normal">إجمالي المدفوع</div>
|
||||||
|
</td>
|
||||||
<td class="text-end pe-4 py-2 fw-bold">{{ settings.currency_symbol }}{{ purchase.paid_amount|floatformat:3 }}</td>
|
<td class="text-end pe-4 py-2 fw-bold">{{ settings.currency_symbol }}{{ purchase.paid_amount|floatformat:3 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="text-danger">
|
<tr class="text-danger">
|
||||||
<td colspan="2" class="border-0"></td>
|
<td colspan="2" class="border-0"></td>
|
||||||
<td class="text-center py-2 fw-bold border-top">{% trans "Balance Due" %}</td>
|
<td class="text-center py-2 fw-bold border-top">
|
||||||
|
<div>{% trans "Balance Due" %}</div>
|
||||||
|
<div class="small fw-normal">الرصيد المستحق</div>
|
||||||
|
</td>
|
||||||
<td class="text-end pe-4 py-2 h5 fw-bold border-top">{{ settings.currency_symbol }}{{ purchase.balance_due|floatformat:3 }}</td>
|
<td class="text-end pe-4 py-2 h5 fw-bold border-top">{{ settings.currency_symbol }}{{ purchase.balance_due|floatformat:3 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@ -127,15 +153,15 @@
|
|||||||
<!-- Payment History -->
|
<!-- Payment History -->
|
||||||
{% if purchase.payments.exists %}
|
{% if purchase.payments.exists %}
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment History" %}</h5>
|
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment History" %} / سجل الدفعات</h5>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-bordered">
|
<table class="table table-sm table-bordered">
|
||||||
<thead class="bg-light small">
|
<thead class="bg-light small">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Date" %}</th>
|
<th>{% trans "Date" %} / التاريخ</th>
|
||||||
<th>{% trans "Method" %}</th>
|
<th>{% trans "Method" %} / الطريقة</th>
|
||||||
<th>{% trans "Amount" %}</th>
|
<th>{% trans "Amount" %} / المبلغ</th>
|
||||||
<th>{% trans "Notes" %}</th>
|
<th>{% trans "Notes" %} / ملاحظات</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="small">
|
<tbody class="small">
|
||||||
@ -156,27 +182,64 @@
|
|||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
{% if purchase.notes %}
|
{% if purchase.notes %}
|
||||||
<div class="bg-light p-4 rounded-3 mb-5">
|
<div class="bg-light p-4 rounded-3 mb-5">
|
||||||
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Notes" %}</h6>
|
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Notes" %} / ملاحظات</h6>
|
||||||
<p class="mb-0 small">{{ purchase.notes }}</p>
|
<p class="mb-0 small">{{ purchase.notes }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="text-center text-muted small mt-5 border-top pt-4">
|
<div class="text-center text-muted small mt-5 border-top pt-4">
|
||||||
<p class="mb-0">{% trans "Thank you for your business!" %}</p>
|
<p class="mb-0">{% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function downloadPDF() {
|
||||||
|
const element = document.getElementById('purchase-card');
|
||||||
|
const opt = {
|
||||||
|
margin: 0,
|
||||||
|
filename: 'Purchase_{{ purchase.invoice_number|default:purchase.id }}.pdf',
|
||||||
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
|
html2canvas: { scale: 2, useCORS: true, letterRendering: true },
|
||||||
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||||
|
};
|
||||||
|
html2pdf().set(opt).from(element).save();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@media print {
|
@media print {
|
||||||
@page { size: portrait; margin: 0; }
|
@page {
|
||||||
body { background-color: white !important; }
|
size: A4 portrait;
|
||||||
.container { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; }
|
margin: 0;
|
||||||
.card { box-shadow: none !important; border: none !important; }
|
}
|
||||||
|
body {
|
||||||
|
background-color: white !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 210mm !important;
|
||||||
|
max-width: 210mm !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
#purchase-card {
|
||||||
|
width: 210mm !important;
|
||||||
|
min-height: 297mm !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
.d-print-none { display: none !important; }
|
.d-print-none { display: none !important; }
|
||||||
.p-5 { padding: 2rem !important; }
|
.p-5 { padding: 15mm !important; }
|
||||||
|
.table-responsive { overflow: visible !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
255
core/templates/core/purchase_return_create.html
Normal file
255
core/templates/core/purchase_return_create.html
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "New Purchase Return" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-4" id="returnApp">
|
||||||
|
<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-arrow-return-right me-2 text-primary"></i>{% trans "Create Purchase Return" %}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<!-- Supplier & Return Info -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<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-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Original Purchase #" %}</label>
|
||||||
|
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="purchaseId">
|
||||||
|
<option value="">{% trans "None / Manual" %}</option>
|
||||||
|
{% for purchase in purchases %}
|
||||||
|
<option value="{{ purchase.id }}">#{{ purchase.invoice_number|default:purchase.id }} ({{ purchase.created_at|date:"Y-m-d" }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Return #" %}</label>
|
||||||
|
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="returnNumber" placeholder="{% trans 'e.g. PRET-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.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 "Search and add products to this return." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Return 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 "Return Summary" %}</h5>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">{% trans "Total Amount" %}</span>
|
||||||
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Reason for Return / Notes" %}</label>
|
||||||
|
<textarea class="form-control rounded-3" rows="4" v-model="notes" placeholder="{% trans 'e.g. Expired product, incorrect shipment...' %}"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-danger border-0 rounded-3 small mb-4">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
{% trans "Completing this return will automatically decrease the stock quantity for the selected items." %}
|
||||||
|
</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="saveReturn">
|
||||||
|
<span v-if="isProcessing" class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
<i class="bi bi-check-circle me-2" v-else></i>
|
||||||
|
{% trans "Process Purchase Return" %}
|
||||||
|
</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: '',
|
||||||
|
purchaseId: '',
|
||||||
|
returnNumber: '',
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
saveReturn() {
|
||||||
|
if (!confirm("{% trans 'Are you sure you want to process this purchase return? This will deduct from stock.' %}")) return;
|
||||||
|
|
||||||
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
supplier_id: this.supplierId,
|
||||||
|
purchase_id: this.purchaseId,
|
||||||
|
return_number: this.returnNumber,
|
||||||
|
items: this.cart.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
price: item.price,
|
||||||
|
line_total: item.price * item.quantity
|
||||||
|
})),
|
||||||
|
total_amount: this.subtotal,
|
||||||
|
notes: this.notes
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("{% url 'create_purchase_return_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 'purchase_returns' %}";
|
||||||
|
} else {
|
||||||
|
alert("Error: " + data.error);
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert("An unexpected error occurred.");
|
||||||
|
this.isProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#returnApp');
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
183
core/templates/core/purchase_return_detail.html
Normal file
183
core/templates/core/purchase_return_detail.html
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Purchase Return" %} #{{ purchase_return.return_number|default:purchase_return.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 'purchase_returns' %}" class="btn btn-light rounded-3">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Returns" %} / العودة إلى المرتجعات
|
||||||
|
</a>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
|
||||||
|
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
|
||||||
|
</button>
|
||||||
|
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
|
||||||
|
<i class="bi bi-printer me-2"></i>{% trans "Print Return" %} / طباعة المرتجع
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Return Content -->
|
||||||
|
<div id="invoice-card" class="card border-0 shadow-sm rounded-4 overflow-hidden mx-auto" style="max-width: 800px;">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 text-sm-end" dir="rtl">
|
||||||
|
<h1 class="fw-bold text-uppercase text-danger opacity-50 mb-4" dir="ltr">{% trans "Purchase Return" %} / مرتجع مشتريات</h1>
|
||||||
|
<div class="mb-4 text-sm-end" dir="ltr">
|
||||||
|
<div class="fw-bold text-dark text-uppercase small">{% trans "Return Number" %} / رقم المرتجع</div>
|
||||||
|
<div class="h5">{{ purchase_return.return_number|default:purchase_return.id }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 text-sm-end" dir="ltr">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Return Date" %} / تاريخ المرتجع</div>
|
||||||
|
<div>{{ purchase_return.created_at|date:"Y-m-d" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Original Purchase" %} / الشراء الأصلي</div>
|
||||||
|
<div>{% if purchase_return.purchase %}#{{ purchase_return.purchase.invoice_number|default:purchase_return.purchase.id }}{% else %}-{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<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_return.supplier.name|default:"N/A" }}</div>
|
||||||
|
{% if purchase_return.supplier.phone %}
|
||||||
|
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ purchase_return.supplier.phone }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</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">
|
||||||
|
<div class="small text-muted">{% trans "Item Description" %}</div>
|
||||||
|
<div class="small">وصف العنصر</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Cost Price" %}</div>
|
||||||
|
<div class="small">سعر التكلفة</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Quantity" %}</div>
|
||||||
|
<div class="small">الكمية</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-end pe-4 border-0">
|
||||||
|
<div class="small text-muted">{% trans "Total" %}</div>
|
||||||
|
<div class="small">المجموع</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in purchase_return.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-danger">{{ 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">
|
||||||
|
<div>{% trans "Total Credit" %}</div>
|
||||||
|
<div class="small fw-normal">إجمالي الرصيد المسترد</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-4 py-3 h5 fw-bold text-danger border-top">{{ settings.currency_symbol }}{{ purchase_return.total_amount|floatformat:3 }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
{% if purchase_return.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_return.notes }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="text-center text-muted small mt-5 border-top pt-4">
|
||||||
|
<p class="mb-1">{% trans "Purchase Return Confirmation" %} / تأكيد مرتجع مشتريات</p>
|
||||||
|
<p class="mb-0">{% trans "Software by Meezan" %} / برمجة ميزان</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function downloadPDF() {
|
||||||
|
const element = document.getElementById('invoice-card');
|
||||||
|
const opt = {
|
||||||
|
margin: 0,
|
||||||
|
filename: 'PurchaseReturn_{{ purchase_return.return_number|default:purchase_return.id }}.pdf',
|
||||||
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
|
html2canvas: { scale: 2, useCORS: true, letterRendering: true },
|
||||||
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||||
|
};
|
||||||
|
html2pdf().set(opt).from(element).save();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
size: A4 portrait;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: white !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 210mm !important;
|
||||||
|
max-width: 210mm !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
#invoice-card {
|
||||||
|
width: 210mm !important;
|
||||||
|
min-height: 297mm !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.d-print-none { display: none !important; }
|
||||||
|
.p-5 { padding: 15mm !important; }
|
||||||
|
.table-responsive { overflow: visible !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
105
core/templates/core/purchase_returns.html
Normal file
105
core/templates/core/purchase_returns.html
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Purchase Returns" %} | {{ 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 "Purchase Returns" %}</h2>
|
||||||
|
<p class="text-muted small mb-0">{% trans "Manage returns to suppliers" %}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'purchase_return_create' %}" class="btn btn-primary rounded-3 px-4 shadow-sm">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>{% trans "New Purchase Return" %}
|
||||||
|
</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 "Return #" %}</th>
|
||||||
|
<th>{% trans "Date" %}</th>
|
||||||
|
<th>{% trans "Supplier" %}</th>
|
||||||
|
<th>{% trans "Original Purchase" %}</th>
|
||||||
|
<th>{% trans "Total Amount" %}</th>
|
||||||
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for return in returns %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-bold">
|
||||||
|
{{ return.return_number|default:return.id }}
|
||||||
|
</td>
|
||||||
|
<td>{{ return.created_at|date:"Y-m-d" }}</td>
|
||||||
|
<td>{{ return.supplier.name|default:"N/A" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if return.purchase %}
|
||||||
|
<a href="{% url 'purchase_detail' return.purchase.id %}" class="text-decoration-none">
|
||||||
|
#{{ return.purchase.invoice_number|default:return.purchase.id }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="fw-bold text-dark">{{ site_settings.currency_symbol }}{{ return.total_amount|floatformat:3 }}</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<div class="btn-group shadow-sm rounded-3">
|
||||||
|
<a href="{% url 'purchase_return_detail' return.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
|
||||||
|
<i class="bi bi-printer"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-white border text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ return.id }}">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Modal -->
|
||||||
|
<div class="modal fade text-start" id="deleteModal{{ return.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 Return?" %}</h4>
|
||||||
|
<p class="text-muted">{% trans "This will restore the quantities to stock. This action cannot be undone." %}</p>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'delete_purchase_return' return.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="6" 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 purchase returns found." %}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
272
core/templates/core/quotation_create.html
Normal file
272
core/templates/core/quotation_create.html
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "New Quotation" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-4" id="quotationApp">
|
||||||
|
<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-file-earmark-text me-2 text-primary"></i>{% trans "Create New Quotation" %}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<!-- Customer & Quotation 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 "Quotation #" %}</label>
|
||||||
|
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="quotationNumber" placeholder="{% trans 'e.g. QUO-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 quotation." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Terms and Conditions" %}</label>
|
||||||
|
<textarea class="form-control rounded-3" rows="4" v-model="termsAndConditions" placeholder="{% trans 'Enter quotation terms, delivery info, etc.' %}"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quotation 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 "Quotation 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>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Valid Until" %}</label>
|
||||||
|
<input type="date" class="form-control rounded-3" v-model="validUntil">
|
||||||
|
</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" @click="saveQuotation">
|
||||||
|
<span v-if="isProcessing" class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
<i class="bi bi-check-circle me-2" v-else></i>
|
||||||
|
{% trans "Save Quotation" %}
|
||||||
|
</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: '',
|
||||||
|
quotationNumber: '',
|
||||||
|
discount: 0,
|
||||||
|
validUntil: '',
|
||||||
|
termsAndConditions: '1. Prices are valid for 7 days.\n2. Delivery within 3-5 working days.\n3. Payment: 50% advance, 50% on delivery.',
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
saveQuotation() {
|
||||||
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
customer_id: this.customerId,
|
||||||
|
quotation_number: this.quotationNumber,
|
||||||
|
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,
|
||||||
|
valid_until: this.validUntil,
|
||||||
|
terms_and_conditions: this.termsAndConditions,
|
||||||
|
notes: this.notes
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("{% url 'create_quotation_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 'quotations' %}";
|
||||||
|
} else {
|
||||||
|
alert("Error: " + data.error);
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert("An unexpected error occurred.");
|
||||||
|
this.isProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#quotationApp');
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
253
core/templates/core/quotation_detail.html
Normal file
253
core/templates/core/quotation_detail.html
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Quotation" %} #{{ quotation.quotation_number|default:quotation.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 'quotations' %}" class="btn btn-light rounded-3">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Quotations" %} / العودة لعروض الأسعار
|
||||||
|
</a>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
{% if quotation.status != 'converted' %}
|
||||||
|
<button type="button" class="btn btn-outline-success rounded-3 px-4" data-bs-toggle="modal" data-bs-target="#convertModal">
|
||||||
|
<i class="bi bi-arrow-right-circle me-2"></i>{% trans "Convert to Invoice" %} / تحويل إلى فاتورة
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
|
||||||
|
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
|
||||||
|
</button>
|
||||||
|
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
|
||||||
|
<i class="bi bi-printer me-2"></i>{% trans "Print Quotation" %} / طباعة عرض السعر
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Convert Modal -->
|
||||||
|
<div class="modal fade d-print-none" id="convertModal" 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-primary mb-3">
|
||||||
|
<i class="bi bi-question-circle" style="font-size: 3rem;"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="fw-bold">{% trans "Convert to Invoice?" %}</h4>
|
||||||
|
<p class="text-muted">{% trans "This will create a sales invoice and deduct items from stock. This action will change the quotation status to Converted." %}</p>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'convert_quotation_to_invoice' quotation.id %}" class="btn btn-primary rounded-3 py-2">{% trans "Yes, Convert to Invoice" %}</a>
|
||||||
|
<button type="button" class="btn btn-light rounded-3 py-2" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quotation Content -->
|
||||||
|
<div id="quotation-card" class="card border-0 shadow-sm rounded-4 overflow-hidden mx-auto" style="max-width: 800px;">
|
||||||
|
<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 "Quotation" %} / عرض سعر</h1>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="fw-bold text-dark text-uppercase small">{% trans "Quotation Number" %} / رقم العرض</div>
|
||||||
|
<div class="h5">{{ quotation.quotation_number|default:quotation.id }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Date" %} / التاريخ</div>
|
||||||
|
<div>{{ quotation.created_at|date:"Y-m-d" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Valid Until" %} / صالح لغاية</div>
|
||||||
|
<div>{{ quotation.valid_until|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 "Quote For" %} / عرض مقدم إلى</div>
|
||||||
|
<div class="h5 fw-bold mb-1">{{ quotation.customer.name|default:_("Guest Customer") }}</div>
|
||||||
|
{% if quotation.customer.phone %}
|
||||||
|
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ quotation.customer.phone }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if quotation.customer.address %}
|
||||||
|
<div class="text-muted small"><i class="bi bi-geo-alt me-2"></i>{{ quotation.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 "Status" %} / الحالة</div>
|
||||||
|
<div>
|
||||||
|
{% if quotation.status == 'converted' %}
|
||||||
|
<span class="h5 badge bg-primary text-white rounded-pill px-4">{% trans "Converted" %} / محول</span>
|
||||||
|
{% elif quotation.status == 'accepted' %}
|
||||||
|
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Accepted" %} / مقبول</span>
|
||||||
|
{% elif quotation.status == 'rejected' %}
|
||||||
|
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Rejected" %} / مرفوض</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="h5 badge bg-info text-white rounded-pill px-4">{% trans "Open" %} / مفتوح</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">
|
||||||
|
<div class="small text-muted">{% trans "Item Description" %}</div>
|
||||||
|
<div class="small">وصف العنصر</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Unit Price" %}</div>
|
||||||
|
<div class="small">سعر الوحدة</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Quantity" %}</div>
|
||||||
|
<div class="small">الكمية</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-end pe-4 border-0">
|
||||||
|
<div class="small text-muted">{% trans "Total" %}</div>
|
||||||
|
<div class="small">المجموع</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in quotation.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">
|
||||||
|
<div>{% trans "Subtotal" %}</div>
|
||||||
|
<div class="small fw-normal">المجموع الفرعي</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-4 py-3 fw-bold border-top">{{ settings.currency_symbol }}{{ quotation.total_amount|add:quotation.discount|floatformat:3 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if quotation.discount > 0 %}
|
||||||
|
<tr class="text-muted">
|
||||||
|
<td colspan="2" class="border-0"></td>
|
||||||
|
<td class="text-center py-2 fw-bold">
|
||||||
|
<div>{% trans "Discount" %}</div>
|
||||||
|
<div class="small fw-normal">الخصم</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-4 py-2 fw-bold">-{{ settings.currency_symbol }}{{ quotation.discount|floatformat:3 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="border-0"></td>
|
||||||
|
<td class="text-center py-3 fw-bold border-top">
|
||||||
|
<div>{% trans "Grand Total" %}</div>
|
||||||
|
<div class="small fw-normal">المجموع الكلي</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-4 py-3 h5 fw-bold text-primary border-top">{{ settings.currency_symbol }}{{ quotation.total_amount|floatformat:3 }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terms & Conditions -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<h6 class="fw-bold small text-uppercase mb-3 text-muted border-bottom pb-2">{% trans "Terms and Conditions" %} / الشروط والأحكام</h6>
|
||||||
|
<div class="small text-muted" style="white-space: pre-line;">
|
||||||
|
{{ quotation.terms_and_conditions|default:_("No specific terms provided.") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
{% if quotation.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">{{ quotation.notes }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="text-center text-muted small mt-5 border-top pt-4">
|
||||||
|
<p class="mb-1">{% trans "This is a computer generated quotation." %} / هذا عرض سعر تم إنشاؤه بواسطة الكمبيوتر.</p>
|
||||||
|
<p class="mb-0">{% trans "Software by Meezan" %} / برمجة ميزان</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function downloadPDF() {
|
||||||
|
const element = document.getElementById('quotation-card');
|
||||||
|
const opt = {
|
||||||
|
margin: 0,
|
||||||
|
filename: 'Quotation_{{ quotation.quotation_number|default:quotation.id }}.pdf',
|
||||||
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
|
html2canvas: { scale: 2, useCORS: true, letterRendering: true },
|
||||||
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||||
|
};
|
||||||
|
html2pdf().set(opt).from(element).save();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
size: A4 portrait;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: white !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 210mm !important;
|
||||||
|
max-width: 210mm !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
#quotation-card {
|
||||||
|
width: 210mm !important;
|
||||||
|
min-height: 297mm !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.d-print-none { display: none !important; }
|
||||||
|
.p-5 { padding: 15mm !important; }
|
||||||
|
.table-responsive { overflow: visible !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
135
core/templates/core/quotations.html
Normal file
135
core/templates/core/quotations.html
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Quotations" %} | {{ 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 "Quotations" %}</h2>
|
||||||
|
<p class="text-muted small mb-0">{% trans "Manage and track your price proposals" %}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'quotation_create' %}" class="btn btn-primary rounded-3 px-4 shadow-sm">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>{% trans "New Quotation" %}
|
||||||
|
</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 "Quotation #" %}</th>
|
||||||
|
<th>{% trans "Date" %}</th>
|
||||||
|
<th>{% trans "Customer" %}</th>
|
||||||
|
<th>{% trans "Total" %}</th>
|
||||||
|
<th>{% trans "Status" %}</th>
|
||||||
|
<th>{% trans "Valid Until" %}</th>
|
||||||
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for q in quotations %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-bold">
|
||||||
|
{{ q.quotation_number|default:q.id }}
|
||||||
|
</td>
|
||||||
|
<td>{{ q.created_at|date:"Y-m-d" }}</td>
|
||||||
|
<td>{{ q.customer.name|default:_("Guest") }}</td>
|
||||||
|
<td class="fw-bold text-dark">{{ site_settings.currency_symbol }}{{ q.total_amount|floatformat:3 }}</td>
|
||||||
|
<td>
|
||||||
|
{% if q.status == 'draft' %}
|
||||||
|
<span class="badge bg-secondary-subtle text-secondary rounded-pill px-3">{% trans "Draft" %}</span>
|
||||||
|
{% elif q.status == 'sent' %}
|
||||||
|
<span class="badge bg-info-subtle text-info rounded-pill px-3">{% trans "Sent" %}</span>
|
||||||
|
{% elif q.status == 'accepted' %}
|
||||||
|
<span class="badge bg-success-subtle text-success rounded-pill px-3">{% trans "Accepted" %}</span>
|
||||||
|
{% elif q.status == 'converted' %}
|
||||||
|
<span class="badge bg-primary-subtle text-primary rounded-pill px-3">{% trans "Converted" %}</span>
|
||||||
|
{% elif q.status == 'rejected' %}
|
||||||
|
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">{% trans "Rejected" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ q.valid_until|date:"Y-m-d"|default:"-" }}</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<div class="btn-group shadow-sm rounded-3">
|
||||||
|
<a href="{% url 'quotation_detail' q.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
|
||||||
|
<i class="bi bi-printer"></i>
|
||||||
|
</a>
|
||||||
|
{% if q.status != 'converted' %}
|
||||||
|
<button type="button" class="btn btn-sm btn-white border text-success" data-bs-toggle="modal" data-bs-target="#convertModal{{ q.id }}" title="{% trans 'Convert to Invoice' %}">
|
||||||
|
<i class="bi bi-arrow-right-circle"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="button" class="btn btn-sm btn-white border text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ q.id }}">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Convert Modal -->
|
||||||
|
<div class="modal fade text-start" id="convertModal{{ q.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-primary mb-3">
|
||||||
|
<i class="bi bi-question-circle" style="font-size: 3rem;"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="fw-bold">{% trans "Convert to Invoice?" %}</h4>
|
||||||
|
<p class="text-muted">{% trans "This will create a sales invoice and deduct items from stock. You won't be able to undo this easily." %}</p>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'convert_quotation_to_invoice' q.id %}" class="btn btn-primary rounded-3 py-2">{% trans "Yes, Convert to Invoice" %}</a>
|
||||||
|
<button type="button" class="btn btn-light rounded-3 py-2" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Modal -->
|
||||||
|
<div class="modal fade text-start" id="deleteModal{{ q.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 Quotation?" %}</h4>
|
||||||
|
<p class="text-muted">{% trans "Are you sure you want to delete this quotation? This action cannot be undone." %}</p>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'delete_quotation' q.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="7" class="text-center py-5">
|
||||||
|
<img src="https://illustrations.popsy.co/gray/work-from-home.svg" alt="Empty" style="width: 200px;" class="mb-3">
|
||||||
|
<p class="text-muted">{% trans "No quotations found." %}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
255
core/templates/core/sale_return_create.html
Normal file
255
core/templates/core/sale_return_create.html
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "New Sales Return" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-4" id="returnApp">
|
||||||
|
<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-arrow-return-left me-2 text-primary"></i>{% trans "Create Sales Return" %}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<!-- Customer & Return Info -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<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-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Original Sale #" %}</label>
|
||||||
|
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="saleId">
|
||||||
|
<option value="">{% trans "None / Manual" %}</option>
|
||||||
|
{% for sale in sales %}
|
||||||
|
<option value="{{ sale.id }}">#{{ sale.invoice_number|default:sale.id }} ({{ sale.created_at|date:"Y-m-d" }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Return #" %}</label>
|
||||||
|
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="returnNumber" placeholder="{% trans 'e.g. RET-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 return." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Return 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 "Return Summary" %}</h5>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">{% trans "Total Amount" %}</span>
|
||||||
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Reason for Return / Notes" %}</label>
|
||||||
|
<textarea class="form-control rounded-3" rows="4" v-model="notes" placeholder="{% trans 'e.g. Damaged product, customer changed mind...' %}"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning border-0 rounded-3 small mb-4">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
{% trans "Completing this return will automatically increase the stock quantity for the selected items." %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button class="btn btn-primary rounded-3 py-3 fw-bold shadow-sm" :disabled="isProcessing || cart.length === 0" @click="saveReturn">
|
||||||
|
<span v-if="isProcessing" class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
<i class="bi bi-check-circle me-2" v-else></i>
|
||||||
|
{% trans "Process Sales Return" %}
|
||||||
|
</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: '',
|
||||||
|
saleId: '',
|
||||||
|
returnNumber: '',
|
||||||
|
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.price,
|
||||||
|
quantity: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.filteredProducts = [];
|
||||||
|
},
|
||||||
|
removeItem(index) {
|
||||||
|
this.cart.splice(index, 1);
|
||||||
|
},
|
||||||
|
saveReturn() {
|
||||||
|
if (!confirm("{% trans 'Are you sure you want to process this return? This will update stock.' %}")) return;
|
||||||
|
|
||||||
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
customer_id: this.customerId,
|
||||||
|
sale_id: this.saleId,
|
||||||
|
return_number: this.returnNumber,
|
||||||
|
items: this.cart.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
price: item.price,
|
||||||
|
line_total: item.price * item.quantity
|
||||||
|
})),
|
||||||
|
total_amount: this.subtotal,
|
||||||
|
notes: this.notes
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("{% url 'create_sale_return_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 'sales_returns' %}";
|
||||||
|
} else {
|
||||||
|
alert("Error: " + data.error);
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert("An unexpected error occurred.");
|
||||||
|
this.isProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#returnApp');
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
183
core/templates/core/sale_return_detail.html
Normal file
183
core/templates/core/sale_return_detail.html
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Sales Return" %} #{{ sale_return.return_number|default:sale_return.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 'sales_returns' %}" class="btn btn-light rounded-3">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Returns" %} / العودة إلى المرتجعات
|
||||||
|
</a>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
|
||||||
|
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
|
||||||
|
</button>
|
||||||
|
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
|
||||||
|
<i class="bi bi-printer me-2"></i>{% trans "Print Return" %} / طباعة المرتجع
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Return Content -->
|
||||||
|
<div id="invoice-card" class="card border-0 shadow-sm rounded-4 overflow-hidden mx-auto" style="max-width: 800px;">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 text-sm-end" dir="rtl">
|
||||||
|
<h1 class="fw-bold text-uppercase text-danger opacity-50 mb-4" dir="ltr">{% trans "Sales Return" %} / مرتجع مبيعات</h1>
|
||||||
|
<div class="mb-4 text-sm-end" dir="ltr">
|
||||||
|
<div class="fw-bold text-dark text-uppercase small">{% trans "Return Number" %} / رقم المرتجع</div>
|
||||||
|
<div class="h5">{{ sale_return.return_number|default:sale_return.id }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 text-sm-end" dir="ltr">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Return Date" %} / تاريخ المرتجع</div>
|
||||||
|
<div>{{ sale_return.created_at|date:"Y-m-d" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="small text-muted fw-bold text-uppercase">{% trans "Original Sale" %} / البيع الأصلي</div>
|
||||||
|
<div>{% if sale_return.sale %}#{{ sale_return.sale.invoice_number|default:sale_return.sale.id }}{% else %}-{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<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_return.customer.name|default:_("Guest Customer") }}</div>
|
||||||
|
{% if sale_return.customer.phone %}
|
||||||
|
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ sale_return.customer.phone }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</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">
|
||||||
|
<div class="small text-muted">{% trans "Item Description" %}</div>
|
||||||
|
<div class="small">وصف العنصر</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Unit Price" %}</div>
|
||||||
|
<div class="small">سعر الوحدة</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-center border-0">
|
||||||
|
<div class="small text-muted">{% trans "Quantity" %}</div>
|
||||||
|
<div class="small">الكمية</div>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 text-end pe-4 border-0">
|
||||||
|
<div class="small text-muted">{% trans "Total" %}</div>
|
||||||
|
<div class="small">المجموع</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in sale_return.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-danger">{{ 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">
|
||||||
|
<div>{% trans "Total Refund" %}</div>
|
||||||
|
<div class="small fw-normal">إجمالي المبلغ المرتجع</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-4 py-3 h5 fw-bold text-danger border-top">{{ settings.currency_symbol }}{{ sale_return.total_amount|floatformat:3 }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
{% if sale_return.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">{{ sale_return.notes }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="text-center text-muted small mt-5 border-top pt-4">
|
||||||
|
<p class="mb-1">{% trans "Sales Return Confirmation" %} / تأكيد مرتجع مبيعات</p>
|
||||||
|
<p class="mb-0">{% trans "Software by Meezan" %} / برمجة ميزان</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function downloadPDF() {
|
||||||
|
const element = document.getElementById('invoice-card');
|
||||||
|
const opt = {
|
||||||
|
margin: 0,
|
||||||
|
filename: 'SalesReturn_{{ sale_return.return_number|default:sale_return.id }}.pdf',
|
||||||
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
|
html2canvas: { scale: 2, useCORS: true, letterRendering: true },
|
||||||
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||||
|
};
|
||||||
|
html2pdf().set(opt).from(element).save();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
size: A4 portrait;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: white !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 210mm !important;
|
||||||
|
max-width: 210mm !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
#invoice-card {
|
||||||
|
width: 210mm !important;
|
||||||
|
min-height: 297mm !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.d-print-none { display: none !important; }
|
||||||
|
.p-5 { padding: 15mm !important; }
|
||||||
|
.table-responsive { overflow: visible !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
105
core/templates/core/sales_returns.html
Normal file
105
core/templates/core/sales_returns.html
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Sales Returns" %} | {{ 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 Returns" %}</h2>
|
||||||
|
<p class="text-muted small mb-0">{% trans "Manage customer product returns" %}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'sale_return_create' %}" class="btn btn-primary rounded-3 px-4 shadow-sm">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>{% trans "New Sales Return" %}
|
||||||
|
</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 "Return #" %}</th>
|
||||||
|
<th>{% trans "Date" %}</th>
|
||||||
|
<th>{% trans "Customer" %}</th>
|
||||||
|
<th>{% trans "Original Sale" %}</th>
|
||||||
|
<th>{% trans "Total Amount" %}</th>
|
||||||
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for return in returns %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-bold">
|
||||||
|
{{ return.return_number|default:return.id }}
|
||||||
|
</td>
|
||||||
|
<td>{{ return.created_at|date:"Y-m-d" }}</td>
|
||||||
|
<td>{{ return.customer.name|default:_("Guest") }}</td>
|
||||||
|
<td>
|
||||||
|
{% if return.sale %}
|
||||||
|
<a href="{% url 'invoice_detail' return.sale.id %}" class="text-decoration-none">
|
||||||
|
#{{ return.sale.invoice_number|default:return.sale.id }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="fw-bold text-dark">{{ site_settings.currency_symbol }}{{ return.total_amount|floatformat:3 }}</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<div class="btn-group shadow-sm rounded-3">
|
||||||
|
<a href="{% url 'sale_return_detail' return.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
|
||||||
|
<i class="bi bi-printer"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-white border text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ return.id }}">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Modal -->
|
||||||
|
<div class="modal fade text-start" id="deleteModal{{ return.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 Return?" %}</h4>
|
||||||
|
<p class="text-muted">{% trans "This will deduct the quantities from stock again. This action cannot be undone." %}</p>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'delete_sale_return' return.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="6" 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 returns found." %}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
25
core/urls.py
25
core/urls.py
@ -12,18 +12,40 @@ urlpatterns = [
|
|||||||
path('settings/', views.settings_view, name='settings'),
|
path('settings/', views.settings_view, name='settings'),
|
||||||
|
|
||||||
# Invoices (Sales)
|
# Invoices (Sales)
|
||||||
path('invoices/', views.invoice_list, name='invoices'), # Changed to 'invoices' for consistency with sidebar
|
path('invoices/', views.invoice_list, name='invoices'),
|
||||||
path('invoices/create/', views.invoice_create, name='invoice_create'),
|
path('invoices/create/', views.invoice_create, name='invoice_create'),
|
||||||
path('invoices/<int:pk>/', views.invoice_detail, name='invoice_detail'),
|
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/payment/<int:pk>/', views.add_sale_payment, name='add_sale_payment'),
|
||||||
path('invoices/delete/<int:pk>/', views.delete_sale, name='delete_sale'),
|
path('invoices/delete/<int:pk>/', views.delete_sale, name='delete_sale'),
|
||||||
|
|
||||||
|
# Quotations
|
||||||
|
path('quotations/', views.quotations, name='quotations'),
|
||||||
|
path('quotations/create/', views.quotation_create, name='quotation_create'),
|
||||||
|
path('quotations/<int:pk>/', views.quotation_detail, name='quotation_detail'),
|
||||||
|
path('quotations/convert/<int:pk>/', views.convert_quotation_to_invoice, name='convert_quotation_to_invoice'),
|
||||||
|
path('quotations/delete/<int:pk>/', views.delete_quotation, name='delete_quotation'),
|
||||||
|
path('api/create-quotation/', views.create_quotation_api, name='create_quotation_api'),
|
||||||
|
|
||||||
|
# Sales Returns
|
||||||
|
path('sales/returns/', views.sales_returns, name='sales_returns'),
|
||||||
|
path('sales/returns/create/', views.sale_return_create, name='sale_return_create'),
|
||||||
|
path('sales/returns/<int:pk>/', views.sale_return_detail, name='sale_return_detail'),
|
||||||
|
path('sales/returns/delete/<int:pk>/', views.delete_sale_return, name='delete_sale_return'),
|
||||||
|
path('api/create-sale-return/', views.create_sale_return_api, name='create_sale_return_api'),
|
||||||
|
|
||||||
# Purchases (Invoices)
|
# Purchases (Invoices)
|
||||||
path('purchases/create/', views.purchase_create, name='purchase_create'),
|
path('purchases/create/', views.purchase_create, name='purchase_create'),
|
||||||
path('purchases/<int:pk>/', views.purchase_detail, name='purchase_detail'),
|
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/payment/<int:pk>/', views.add_purchase_payment, name='add_purchase_payment'),
|
||||||
path('purchases/delete/<int:pk>/', views.delete_purchase, name='delete_purchase'),
|
path('purchases/delete/<int:pk>/', views.delete_purchase, name='delete_purchase'),
|
||||||
|
|
||||||
|
# Purchase Returns
|
||||||
|
path('purchases/returns/', views.purchase_returns, name='purchase_returns'),
|
||||||
|
path('purchases/returns/create/', views.purchase_return_create, name='purchase_return_create'),
|
||||||
|
path('purchases/returns/<int:pk>/', views.purchase_return_detail, name='purchase_return_detail'),
|
||||||
|
path('purchases/returns/delete/<int:pk>/', views.delete_purchase_return, name='delete_purchase_return'),
|
||||||
|
path('api/create-purchase-return/', views.create_purchase_return_api, name='create_purchase_return_api'),
|
||||||
|
|
||||||
# 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'),
|
path('api/create-purchase/', views.create_purchase_api, name='create_purchase_api'),
|
||||||
@ -42,6 +64,7 @@ urlpatterns = [
|
|||||||
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/edit/<int:pk>/', views.edit_product, name='edit_product'),
|
||||||
path('inventory/delete/<int:pk>/', views.delete_product, name='delete_product'),
|
path('inventory/delete/<int:pk>/', views.delete_product, name='delete_product'),
|
||||||
|
path('inventory/barcodes/', views.barcode_labels, name='barcode_labels'),
|
||||||
|
|
||||||
# Categories
|
# Categories
|
||||||
path('inventory/category/add/', views.add_category, name='add_category'),
|
path('inventory/category/add/', views.add_category, name='add_category'),
|
||||||
|
|||||||
266
core/views.py
266
core/views.py
@ -6,7 +6,9 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
from .models import (
|
from .models import (
|
||||||
Product, Sale, Category, Unit, Customer, Supplier,
|
Product, Sale, Category, Unit, Customer, Supplier,
|
||||||
Purchase, PurchaseItem, PurchasePayment,
|
Purchase, PurchaseItem, PurchasePayment,
|
||||||
SaleItem, SalePayment, SystemSetting
|
SaleItem, SalePayment, SystemSetting,
|
||||||
|
Quotation, QuotationItem,
|
||||||
|
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem
|
||||||
)
|
)
|
||||||
import json
|
import json
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@ -348,6 +350,263 @@ def delete_sale(request, pk):
|
|||||||
messages.success(request, _("Sale deleted successfully!"))
|
messages.success(request, _("Sale deleted successfully!"))
|
||||||
return redirect('invoices')
|
return redirect('invoices')
|
||||||
|
|
||||||
|
# --- Quotation Views ---
|
||||||
|
|
||||||
|
def quotations(request):
|
||||||
|
quotations_list = Quotation.objects.all().order_by('-created_at')
|
||||||
|
customers = Customer.objects.all()
|
||||||
|
return render(request, 'core/quotations.html', {'quotations': quotations_list, 'customers': customers})
|
||||||
|
|
||||||
|
def quotation_create(request):
|
||||||
|
products = Product.objects.filter(is_active=True)
|
||||||
|
customers = Customer.objects.all()
|
||||||
|
return render(request, 'core/quotation_create.html', {'products': products, 'customers': customers})
|
||||||
|
|
||||||
|
def quotation_detail(request, pk):
|
||||||
|
quotation = get_object_or_404(Quotation, pk=pk)
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
return render(request, 'core/quotation_detail.html', {'quotation': quotation, 'settings': settings})
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def create_quotation_api(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
customer_id = data.get('customer_id')
|
||||||
|
quotation_number = data.get('quotation_number', '')
|
||||||
|
items = data.get('items', [])
|
||||||
|
total_amount = data.get('total_amount', 0)
|
||||||
|
discount = data.get('discount', 0)
|
||||||
|
valid_until = data.get('valid_until')
|
||||||
|
terms_and_conditions = data.get('terms_and_conditions', '')
|
||||||
|
notes = data.get('notes', '')
|
||||||
|
|
||||||
|
customer = None
|
||||||
|
if customer_id:
|
||||||
|
customer = Customer.objects.get(id=customer_id)
|
||||||
|
|
||||||
|
quotation = Quotation.objects.create(
|
||||||
|
customer=customer,
|
||||||
|
quotation_number=quotation_number,
|
||||||
|
total_amount=total_amount,
|
||||||
|
discount=discount,
|
||||||
|
valid_until=valid_until if valid_until else None,
|
||||||
|
terms_and_conditions=terms_and_conditions,
|
||||||
|
notes=notes
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
product = Product.objects.get(id=item['id'])
|
||||||
|
QuotationItem.objects.create(
|
||||||
|
quotation=quotation,
|
||||||
|
product=product,
|
||||||
|
quantity=item['quantity'],
|
||||||
|
unit_price=item['price'],
|
||||||
|
line_total=item['line_total']
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({'success': True, 'quotation_id': quotation.id})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||||
|
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
|
||||||
|
|
||||||
|
def convert_quotation_to_invoice(request, pk):
|
||||||
|
quotation = get_object_or_404(Quotation, pk=pk)
|
||||||
|
if quotation.status == 'converted':
|
||||||
|
messages.warning(request, _("This quotation has already been converted to an invoice."))
|
||||||
|
return redirect('invoices')
|
||||||
|
|
||||||
|
# Create Sale from Quotation
|
||||||
|
sale = Sale.objects.create(
|
||||||
|
customer=quotation.customer,
|
||||||
|
quotation=quotation,
|
||||||
|
total_amount=quotation.total_amount,
|
||||||
|
discount=quotation.discount,
|
||||||
|
balance_due=quotation.total_amount,
|
||||||
|
payment_type='cash',
|
||||||
|
status='unpaid',
|
||||||
|
notes=quotation.notes
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create SaleItems and Update Stock
|
||||||
|
for item in quotation.items.all():
|
||||||
|
SaleItem.objects.create(
|
||||||
|
sale=sale,
|
||||||
|
product=item.product,
|
||||||
|
quantity=item.quantity,
|
||||||
|
unit_price=item.unit_price,
|
||||||
|
line_total=item.line_total
|
||||||
|
)
|
||||||
|
# Deduct Stock
|
||||||
|
item.product.stock_quantity -= item.quantity
|
||||||
|
item.product.save()
|
||||||
|
|
||||||
|
# Update Quotation Status
|
||||||
|
quotation.status = 'converted'
|
||||||
|
quotation.save()
|
||||||
|
|
||||||
|
messages.success(request, _("Quotation converted to Invoice successfully!"))
|
||||||
|
return redirect('invoice_detail', pk=sale.pk)
|
||||||
|
|
||||||
|
def delete_quotation(request, pk):
|
||||||
|
quotation = get_object_or_404(Quotation, pk=pk)
|
||||||
|
quotation.delete()
|
||||||
|
messages.success(request, _("Quotation deleted successfully!"))
|
||||||
|
return redirect('quotations')
|
||||||
|
|
||||||
|
# --- Sale Return Views ---
|
||||||
|
|
||||||
|
def sales_returns(request):
|
||||||
|
returns = SaleReturn.objects.all().order_by('-created_at')
|
||||||
|
return render(request, 'core/sales_returns.html', {'returns': returns})
|
||||||
|
|
||||||
|
def sale_return_create(request):
|
||||||
|
products = Product.objects.filter(is_active=True)
|
||||||
|
customers = Customer.objects.all()
|
||||||
|
sales = Sale.objects.all().order_by('-created_at')
|
||||||
|
return render(request, 'core/sale_return_create.html', {
|
||||||
|
'products': products,
|
||||||
|
'customers': customers,
|
||||||
|
'sales': sales
|
||||||
|
})
|
||||||
|
|
||||||
|
def sale_return_detail(request, pk):
|
||||||
|
sale_return = get_object_or_404(SaleReturn, pk=pk)
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
return render(request, 'core/sale_return_detail.html', {'sale_return': sale_return, 'settings': settings})
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def create_sale_return_api(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
sale_id = data.get('sale_id')
|
||||||
|
customer_id = data.get('customer_id')
|
||||||
|
return_number = data.get('return_number', '')
|
||||||
|
items = data.get('items', [])
|
||||||
|
total_amount = data.get('total_amount', 0)
|
||||||
|
notes = data.get('notes', '')
|
||||||
|
|
||||||
|
customer = None
|
||||||
|
if customer_id:
|
||||||
|
customer = Customer.objects.get(id=customer_id)
|
||||||
|
|
||||||
|
sale = None
|
||||||
|
if sale_id:
|
||||||
|
sale = Sale.objects.get(id=sale_id)
|
||||||
|
|
||||||
|
sale_return = SaleReturn.objects.create(
|
||||||
|
sale=sale,
|
||||||
|
customer=customer,
|
||||||
|
return_number=return_number,
|
||||||
|
total_amount=total_amount,
|
||||||
|
notes=notes
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
product = Product.objects.get(id=item['id'])
|
||||||
|
SaleReturnItem.objects.create(
|
||||||
|
sale_return=sale_return,
|
||||||
|
product=product,
|
||||||
|
quantity=item['quantity'],
|
||||||
|
unit_price=item['price'],
|
||||||
|
line_total=item['line_total']
|
||||||
|
)
|
||||||
|
# Increase Stock for Sales Return
|
||||||
|
product.stock_quantity += int(item['quantity'])
|
||||||
|
product.save()
|
||||||
|
|
||||||
|
return JsonResponse({'success': True, 'return_id': sale_return.id})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||||
|
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
|
||||||
|
|
||||||
|
def delete_sale_return(request, pk):
|
||||||
|
sale_return = get_object_or_404(SaleReturn, pk=pk)
|
||||||
|
for item in sale_return.items.all():
|
||||||
|
item.product.stock_quantity -= item.quantity
|
||||||
|
item.product.save()
|
||||||
|
sale_return.delete()
|
||||||
|
messages.success(request, _("Sale return deleted successfully!"))
|
||||||
|
return redirect('sales_returns')
|
||||||
|
|
||||||
|
|
||||||
|
# --- Purchase Return Views ---
|
||||||
|
|
||||||
|
def purchase_returns(request):
|
||||||
|
returns = PurchaseReturn.objects.all().order_by('-created_at')
|
||||||
|
return render(request, 'core/purchase_returns.html', {'returns': returns})
|
||||||
|
|
||||||
|
def purchase_return_create(request):
|
||||||
|
products = Product.objects.filter(is_active=True)
|
||||||
|
suppliers = Supplier.objects.all()
|
||||||
|
purchases = Purchase.objects.all().order_by('-created_at')
|
||||||
|
return render(request, 'core/purchase_return_create.html', {
|
||||||
|
'products': products,
|
||||||
|
'suppliers': suppliers,
|
||||||
|
'purchases': purchases
|
||||||
|
})
|
||||||
|
|
||||||
|
def purchase_return_detail(request, pk):
|
||||||
|
purchase_return = get_object_or_404(PurchaseReturn, pk=pk)
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
return render(request, 'core/purchase_return_detail.html', {'purchase_return': purchase_return, 'settings': settings})
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def create_purchase_return_api(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
purchase_id = data.get('purchase_id')
|
||||||
|
supplier_id = data.get('supplier_id')
|
||||||
|
return_number = data.get('return_number', '')
|
||||||
|
items = data.get('items', [])
|
||||||
|
total_amount = data.get('total_amount', 0)
|
||||||
|
notes = data.get('notes', '')
|
||||||
|
|
||||||
|
supplier = None
|
||||||
|
if supplier_id:
|
||||||
|
supplier = Supplier.objects.get(id=supplier_id)
|
||||||
|
|
||||||
|
purchase = None
|
||||||
|
if purchase_id:
|
||||||
|
purchase = Purchase.objects.get(id=purchase_id)
|
||||||
|
|
||||||
|
purchase_return = PurchaseReturn.objects.create(
|
||||||
|
purchase=purchase,
|
||||||
|
supplier=supplier,
|
||||||
|
return_number=return_number,
|
||||||
|
total_amount=total_amount,
|
||||||
|
notes=notes
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
product = Product.objects.get(id=item['id'])
|
||||||
|
PurchaseReturnItem.objects.create(
|
||||||
|
purchase_return=purchase_return,
|
||||||
|
product=product,
|
||||||
|
quantity=item['quantity'],
|
||||||
|
cost_price=item['price'],
|
||||||
|
line_total=item['line_total']
|
||||||
|
)
|
||||||
|
# Decrease Stock for Purchase Return
|
||||||
|
product.stock_quantity -= int(item['quantity'])
|
||||||
|
product.save()
|
||||||
|
|
||||||
|
return JsonResponse({'success': True, 'return_id': purchase_return.id})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||||
|
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
|
||||||
|
|
||||||
|
def delete_purchase_return(request, pk):
|
||||||
|
purchase_return = get_object_or_404(PurchaseReturn, pk=pk)
|
||||||
|
for item in purchase_return.items.all():
|
||||||
|
item.product.stock_quantity += item.quantity
|
||||||
|
item.product.save()
|
||||||
|
purchase_return.delete()
|
||||||
|
messages.success(request, _("Purchase return deleted successfully!"))
|
||||||
|
return redirect('purchase_returns')
|
||||||
|
|
||||||
# --- Other Management Views ---
|
# --- Other Management Views ---
|
||||||
|
|
||||||
def reports(request):
|
def reports(request):
|
||||||
@ -575,3 +834,8 @@ def delete_unit(request, pk):
|
|||||||
unit.delete()
|
unit.delete()
|
||||||
messages.success(request, "Unit deleted successfully!")
|
messages.success(request, "Unit deleted successfully!")
|
||||||
return redirect('inventory')
|
return redirect('inventory')
|
||||||
|
|
||||||
|
def barcode_labels(request):
|
||||||
|
products = Product.objects.filter(is_active=True).order_by('name_en')
|
||||||
|
context = {'products': products}
|
||||||
|
return render(request, 'core/barcode_labels.html', context)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user