improving the system

This commit is contained in:
Flatlogic Bot 2026-02-02 10:11:13 +00:00
parent 5391ba1010
commit f80934e391
23 changed files with 2889 additions and 83 deletions

View File

@ -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')),
],
),
]

View File

@ -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')),
],
),
]

View File

@ -71,6 +71,7 @@ class Sale(models.Model):
]
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)
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)
@ -117,6 +118,38 @@ class SalePayment(models.Model):
def __str__(self):
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):
PAYMENT_TYPE_CHOICES = [
('cash', _('Cash')),
@ -175,6 +208,48 @@ class PurchasePayment(models.Model):
def __str__(self):
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):
business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting")
address = models.TextField(_("Address"), blank=True)

View File

@ -47,19 +47,31 @@
<i class="bi bi-speedometer2"></i> {% trans "Dashboard" %}
</a>
</li>
<li class="mt-3 px-4 small text-muted text-uppercase fw-bold">{% trans "Sales" %}</li>
<li>
<a href="{% url 'pos' %}" class="{% if request.resolver_match.url_name == 'pos' %}active{% endif %}">
<i class="bi bi-shop"></i> {% trans "POS System" %}
</a>
</li>
<li>
<a href="{% url 'invoices' %}" class="{% if request.resolver_match.url_name == 'invoices' %}active{% endif %}">
<i class="bi bi-file-earmark-text"></i> {% trans "Invoices" %}
<a href="{% url 'invoice_create' %}" class="{% if request.resolver_match.url_name == 'invoice_create' %}active{% endif %}">
<i class="bi bi-plus-circle"></i> {% trans "New Sales" %}
</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 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-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>
</li>
@ -70,10 +82,25 @@
</a>
</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" %}
</a>
</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>
@ -93,6 +120,11 @@
<i class="bi bi-gear"></i> {% trans "Settings" %}
</a>
</li>
<li>
<a href="/admin/">
<i class="bi bi-shield-lock"></i> {% trans "Django Admin" %}
</a>
</li>
</ul>
<div class="mt-auto p-4 border-top">

View 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 %}

View File

@ -8,15 +8,20 @@
<!-- Action Bar -->
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
<a href="{% url 'invoices' %}" class="btn btn-light rounded-3">
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Invoices" %}
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Invoices" %} / العودة إلى الفواتير
</a>
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
<i class="bi bi-printer me-2"></i>{% trans "Print Invoice" %}
<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 Invoice" %} / طباعة الفاتورة
</button>
</div>
</div>
<!-- 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">
<!-- Header Section -->
<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-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>
<p class="mb-0"><i class="bi bi-receipt me-2"></i>{% trans "VAT" %} / الضريبة: {{ settings.vat_number }}</p>
{% endif %}
</div>
</div>
<div class="col-sm-6 text-sm-end">
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4">{% trans "Tax Invoice" %}</h1>
<div class="mb-4">
<div class="fw-bold text-dark">{% trans "Invoice Number" %}</div>
<div class="col-sm-6 text-sm-end" dir="rtl">
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4" dir="ltr">{% trans "Tax Invoice" %} / فاتورة ضريبية</h1>
<div class="mb-4 text-sm-end" dir="ltr">
<div class="fw-bold text-dark text-uppercase small">{% trans "Invoice Number" %} / رقم الفاتورة</div>
<div class="h5">{{ sale.invoice_number|default:sale.id }}</div>
</div>
<div class="row g-3">
<div class="row g-3 text-sm-end" dir="ltr">
<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>
<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>
</div>
@ -57,7 +62,7 @@
<div class="row mb-5">
<div class="col-sm-6">
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Customer Information" %}</div>
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Customer Information" %} / معلومات العميل</div>
<div class="h5 fw-bold mb-1">{{ sale.customer.name|default:_("Guest Customer") }}</div>
{% if sale.customer.phone %}
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ sale.customer.phone }}</div>
@ -67,14 +72,14 @@
{% endif %}
</div>
<div class="col-sm-6 text-sm-end">
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Payment Status" %}</div>
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Payment Status" %} / حالة الدفع</div>
<div>
{% if sale.status == 'paid' %}
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %}</span>
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %} / مدفوع بالكامل</span>
{% elif sale.status == 'partial' %}
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %}</span>
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %} / مدفوع جزئياً</span>
{% else %}
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %}</span>
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %} / غير مدفوع</span>
{% endif %}
</div>
</div>
@ -85,10 +90,22 @@
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr>
<th class="py-3 ps-4 border-0">{% trans "Item Description" %}</th>
<th class="py-3 text-center border-0">{% trans "Unit Price" %}</th>
<th class="py-3 text-center border-0">{% trans "Quantity" %}</th>
<th class="py-3 text-end pe-4 border-0">{% trans "Total" %}</th>
<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>
@ -107,29 +124,44 @@
<tfoot>
<tr>
<td colspan="2" class="border-0"></td>
<td class="text-center py-3 fw-bold border-top">{% trans "Subtotal" %}</td>
<td class="text-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>
</tr>
{% if sale.discount > 0 %}
<tr class="text-muted">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold">{% trans "Discount" %}</td>
<td class="text-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>
</tr>
{% endif %}
<tr>
<td colspan="2" class="border-0"></td>
<td class="text-center py-3 fw-bold">{% trans "Grand Total" %}</td>
<td class="text-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>
</tr>
<tr class="text-success">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold">{% trans "Total Paid" %}</td>
<td class="text-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>
</tr>
<tr class="text-danger">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold border-top">{% trans "Balance Due" %}</td>
<td class="text-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>
</tr>
</tfoot>
@ -139,15 +171,15 @@
<!-- Payment History -->
{% if sale.payments.exists %}
<div class="mb-5">
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment Records" %}</h5>
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment Records" %} / سجلات الدفع</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="bg-light small">
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Amount" %}</th>
<th>{% trans "Notes" %}</th>
<th>{% trans "Date" %} / التاريخ</th>
<th>{% trans "Method" %} / الطريقة</th>
<th>{% trans "Amount" %} / المبلغ</th>
<th>{% trans "Notes" %} / ملاحظات</th>
</tr>
</thead>
<tbody class="small">
@ -168,28 +200,65 @@
<!-- Notes -->
{% if sale.notes %}
<div class="bg-light p-4 rounded-3 mb-5">
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Internal Notes" %}</h6>
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Internal Notes" %} / ملاحظات داخلية</h6>
<p class="mb-0 small">{{ sale.notes }}</p>
</div>
{% endif %}
<div class="text-center text-muted small mt-5 border-top pt-4">
<p class="mb-1">{% trans "Thank you for your business!" %}</p>
<p class="mb-0">{% trans "Software by Meezan" %}</p>
<p class="mb-1">{% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!</p>
<p class="mb-0">{% trans "Software by Meezan" %} / برمجة ميزان</p>
</div>
</div>
</div>
</div>
</div>
<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>
@media print {
@page { size: portrait; margin: 0; }
body { background-color: white !important; }
.container { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; }
.card { box-shadow: none !important; border: none !important; }
@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: 2rem !important; }
.p-5 { padding: 15mm !important; }
.table-responsive { overflow: visible !important; }
}
</style>
{% endblock %}

View File

@ -8,15 +8,20 @@
<!-- Action Bar -->
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
<a href="{% url 'purchases' %}" class="btn btn-light rounded-3">
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to List" %}
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to List" %} / العودة للقائمة
</a>
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
<i class="bi bi-printer me-2"></i>{% trans "Print Invoice" %}
<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 Invoice" %} / طباعة الفاتورة
</button>
</div>
</div>
<!-- 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">
<!-- Header Section -->
<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-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>
<p class="mb-0"><i class="bi bi-receipt me-2"></i>{% trans "VAT" %} / الضريبة: {{ settings.vat_number }}</p>
{% endif %}
</div>
</div>
<div class="col-sm-6 text-sm-end">
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4">{% trans "Purchase Invoice" %}</h1>
<h1 class="fw-bold text-uppercase text-muted opacity-50 mb-4">{% trans "Purchase Invoice" %} / فاتورة مشتريات</h1>
<div class="mb-4">
<div class="fw-bold text-dark">{% trans "Invoice Number" %}</div>
<div class="fw-bold text-dark text-uppercase small">{% trans "Invoice Number" %} / رقم الفاتورة</div>
<div class="h5">{{ purchase.invoice_number|default:purchase.id }}</div>
</div>
<div class="row g-3">
<div class="col-6">
<div class="small text-muted fw-bold">{% trans "Issue Date" %}</div>
<div class="small text-muted fw-bold text-uppercase">{% trans "Issue Date" %} / تاريخ الإصدار</div>
<div>{{ purchase.created_at|date:"Y-m-d" }}</div>
</div>
<div class="col-6">
<div class="small text-muted fw-bold">{% trans "Due Date" %}</div>
<div class="small text-muted fw-bold text-uppercase">{% trans "Due Date" %} / تاريخ الاستحقاق</div>
<div>{{ purchase.due_date|date:"Y-m-d"|default:"-" }}</div>
</div>
</div>
@ -57,7 +62,7 @@
<div class="row mb-5">
<div class="col-sm-6">
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Supplier Information" %}</div>
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Supplier Information" %} / معلومات المورد</div>
<div class="h5 fw-bold mb-1">{{ purchase.supplier.name }}</div>
{% if purchase.supplier.phone %}
<div class="text-muted small"><i class="bi bi-telephone me-2"></i>{{ purchase.supplier.phone }}</div>
@ -67,14 +72,14 @@
{% endif %}
</div>
<div class="col-sm-6 text-sm-end">
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Payment Status" %}</div>
<div class="small text-muted fw-bold mb-3 text-uppercase tracking-wider">{% trans "Payment Status" %} / حالة الدفع</div>
<div>
{% if purchase.status == 'paid' %}
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %}</span>
<span class="h5 badge bg-success text-white rounded-pill px-4">{% trans "Fully Paid" %} / مدفوع بالكامل</span>
{% elif purchase.status == 'partial' %}
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %}</span>
<span class="h5 badge bg-warning text-dark rounded-pill px-4">{% trans "Partially Paid" %} / مدفوع جزئياً</span>
{% else %}
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %}</span>
<span class="h5 badge bg-danger text-white rounded-pill px-4">{% trans "Unpaid" %} / غير مدفوع</span>
{% endif %}
</div>
</div>
@ -85,10 +90,22 @@
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr>
<th class="py-3 ps-4 border-0">{% trans "Item Description" %}</th>
<th class="py-3 text-center border-0">{% trans "Cost Price" %}</th>
<th class="py-3 text-center border-0">{% trans "Quantity" %}</th>
<th class="py-3 text-end pe-4 border-0">{% trans "Total" %}</th>
<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>
@ -107,17 +124,26 @@
<tfoot>
<tr>
<td colspan="2" class="border-0"></td>
<td class="text-center py-3 fw-bold border-top">{% trans "Grand Total" %}</td>
<td class="text-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>
</tr>
<tr class="text-success">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold">{% trans "Total Paid" %}</td>
<td class="text-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>
</tr>
<tr class="text-danger">
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold border-top">{% trans "Balance Due" %}</td>
<td class="text-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>
</tr>
</tfoot>
@ -127,15 +153,15 @@
<!-- Payment History -->
{% if purchase.payments.exists %}
<div class="mb-5">
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment History" %}</h5>
<h5 class="fw-bold mb-3"><i class="bi bi-credit-card me-2"></i>{% trans "Payment History" %} / سجل الدفعات</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="bg-light small">
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Amount" %}</th>
<th>{% trans "Notes" %}</th>
<th>{% trans "Date" %} / التاريخ</th>
<th>{% trans "Method" %} / الطريقة</th>
<th>{% trans "Amount" %} / المبلغ</th>
<th>{% trans "Notes" %} / ملاحظات</th>
</tr>
</thead>
<tbody class="small">
@ -156,27 +182,64 @@
<!-- Notes -->
{% if purchase.notes %}
<div class="bg-light p-4 rounded-3 mb-5">
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Notes" %}</h6>
<h6 class="fw-bold small text-uppercase mb-2 text-muted">{% trans "Notes" %} / ملاحظات</h6>
<p class="mb-0 small">{{ purchase.notes }}</p>
</div>
{% endif %}
<div class="text-center text-muted small mt-5 border-top pt-4">
<p class="mb-0">{% trans "Thank you for your business!" %}</p>
<p class="mb-0">{% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!</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('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>
@media print {
@page { size: portrait; margin: 0; }
body { background-color: white !important; }
.container { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; }
.card { box-shadow: none !important; border: none !important; }
@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;
}
#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; }
.p-5 { padding: 2rem !important; }
.p-5 { padding: 15mm !important; }
.table-responsive { overflow: visible !important; }
}
</style>
{% endblock %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@ -12,18 +12,40 @@ urlpatterns = [
path('settings/', views.settings_view, name='settings'),
# Invoices (Sales)
path('invoices/', views.invoice_list, name='invoices'), # Changed to 'invoices' for consistency with sidebar
path('invoices/', views.invoice_list, name='invoices'),
path('invoices/create/', views.invoice_create, name='invoice_create'),
path('invoices/<int:pk>/', views.invoice_detail, name='invoice_detail'),
path('invoices/payment/<int:pk>/', views.add_sale_payment, name='add_sale_payment'),
path('invoices/delete/<int:pk>/', views.delete_sale, name='delete_sale'),
# 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)
path('purchases/create/', views.purchase_create, name='purchase_create'),
path('purchases/<int:pk>/', views.purchase_detail, name='purchase_detail'),
path('purchases/payment/<int:pk>/', views.add_purchase_payment, name='add_purchase_payment'),
path('purchases/delete/<int:pk>/', views.delete_purchase, name='delete_purchase'),
# 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
path('api/create-sale/', views.create_sale_api, name='create_sale_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/edit/<int:pk>/', views.edit_product, name='edit_product'),
path('inventory/delete/<int:pk>/', views.delete_product, name='delete_product'),
path('inventory/barcodes/', views.barcode_labels, name='barcode_labels'),
# Categories
path('inventory/category/add/', views.add_category, name='add_category'),

View File

@ -6,7 +6,9 @@ from django.views.decorators.csrf import csrf_exempt
from .models import (
Product, Sale, Category, Unit, Customer, Supplier,
Purchase, PurchaseItem, PurchasePayment,
SaleItem, SalePayment, SystemSetting
SaleItem, SalePayment, SystemSetting,
Quotation, QuotationItem,
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem
)
import json
from datetime import timedelta
@ -348,6 +350,263 @@ def delete_sale(request, pk):
messages.success(request, _("Sale deleted successfully!"))
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 ---
def reports(request):
@ -575,3 +834,8 @@ def delete_unit(request, pk):
unit.delete()
messages.success(request, "Unit deleted successfully!")
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)