Autosave: 20260202-164535

This commit is contained in:
Flatlogic Bot 2026-02-02 16:45:35 +00:00
parent e9c5a5c213
commit 0ae32328a7
15 changed files with 495 additions and 58 deletions

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-02 16:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_systemsetting_decimal_places'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HeldSale',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cart_data', models.JSONField(verbose_name='Cart Data')),
('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')),
('notes', models.TextField(blank=True, verbose_name='Notes')),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='held_sales', to=settings.AUTH_USER_MODEL)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='held_sales', to='core.customer')),
],
),
]

View File

@ -268,6 +268,17 @@ class PurchaseReturnItem(models.Model):
def __str__(self): def __str__(self):
return f"{self.product.name_en} x {self.quantity}" return f"{self.product.name_en} x {self.quantity}"
class HeldSale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales")
cart_data = models.JSONField(_("Cart Data"))
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
notes = models.TextField(_("Notes"), blank=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Held Sale #{self.id} - {self.created_at.strftime('%Y-%m-%d %H:%M')}"
class SystemSetting(models.Model): class SystemSetting(models.Model):
business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting") business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting")
address = models.TextField(_("Address"), blank=True) address = models.TextField(_("Address"), blank=True)
@ -281,4 +292,4 @@ class SystemSetting(models.Model):
registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True) registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True)
def __str__(self): def __str__(self):
return self.business_name return self.business_name

View File

@ -78,7 +78,7 @@
<select class="form-select"> <select class="form-select">
<option value="">{% trans "All Categories" %}</option> <option value="">{% trans "All Categories" %}</option>
{% for category in categories %} {% for category in categories %}
<option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option> <option value="{{ category.id }}">{{ category.name_ar }} / {{ category.name_en }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -118,13 +118,14 @@
</div> </div>
{% endif %} {% endif %}
<div> <div>
<div class="fw-bold text-dark">{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}</div> <div class="fw-bold text-dark" dir="rtl">{{ product.name_ar }}</div>
<small class="text-muted">{{ product.unit.name_en|default:"" }}</small> <div class="small text-muted">{{ product.name_en }}</div>
<small class="text-muted">{{ product.unit.name_ar|default:"" }} / {{ product.unit.name_en|default:"" }}</small>
</div> </div>
</div> </div>
</td> </td>
<td><code>{{ product.sku }}</code></td> <td><code>{{ product.sku }}</code></td>
<td>{% if LANGUAGE_CODE == 'ar' %}{{ product.category.name_ar }}{% else %}{{ product.category.name_en }}{% endif %}</td> <td>{{ product.category.name_ar }} / {{ product.category.name_en }}</td>
<td> <td>
<span class="badge {% if product.stock_quantity < 5 %}bg-danger{% else %}bg-success-subtle text-success{% endif %} rounded-pill"> <span class="badge {% if product.stock_quantity < 5 %}bg-danger{% else %}bg-success-subtle text-success{% endif %} rounded-pill">
{{ product.stock_quantity }} {{ product.unit.short_name|default:"" }} {{ product.stock_quantity }} {{ product.unit.short_name|default:"" }}
@ -348,7 +349,7 @@
<select name="category" id="categorySelect" class="form-select rounded-3" required> <select name="category" id="categorySelect" class="form-select rounded-3" required>
<option value="">{% trans "Select Category" %}</option> <option value="">{% trans "Select Category" %}</option>
{% for category in categories %} {% for category in categories %}
<option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option> <option value="{{ category.id }}">{{ category.name_ar }} / {{ category.name_en }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -357,7 +358,7 @@
<select name="unit" id="unitSelect" class="form-select rounded-3"> <select name="unit" id="unitSelect" class="form-select rounded-3">
<option value="">{% trans "Select Unit" %}</option> <option value="">{% trans "Select Unit" %}</option>
{% for unit in units %} {% for unit in units %}
<option value="{{ unit.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ unit.name_ar }}{% else %}{{ unit.name_en }}{% endif %}</option> <option value="{{ unit.id }}">{{ unit.name_ar }} / {{ unit.name_en }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View File

@ -112,8 +112,8 @@
{% for item in sale.items.all %} {% for item in sale.items.all %}
<tr> <tr>
<td class="py-3 ps-4"> <td class="py-3 ps-4">
<div class="fw-bold">{{ item.product.name_en }}</div> <div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
<div class="text-muted small">{{ item.product.name_ar }}</div> <div class="text-muted small">{{ item.product.name_en }}</div>
</td> </td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}</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-center">{{ item.quantity }}</td>
@ -189,7 +189,7 @@
<td>{{ payment.payment_date|date:"Y-m-d" }}</td> <td>{{ payment.payment_date|date:"Y-m-d" }}</td>
<td> <td>
{% if payment.payment_method %} {% if payment.payment_method %}
{% if LANGUAGE_CODE == 'ar' %}{{ payment.payment_method.name_ar }}{% else %}{{ payment.payment_method.name_en }}{% endif %} {{ payment.payment_method.name_ar }} / {{ payment.payment_method.name_en }}
{% else %} {% else %}
{{ payment.payment_method_name }} {{ payment.payment_method_name }}
{% endif %} {% endif %}

View File

@ -48,11 +48,14 @@
.product-name { .product-name {
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1.2; line-height: 1.2;
height: 2.4em; height: auto;
overflow: hidden; overflow: hidden;
display: -webkit-box; }
-webkit-line-clamp: 2;
-webkit-box-orient: vertical; .payment-method-btn.active {
background-color: var(--bs-primary);
color: white;
border-color: var(--bs-primary);
} }
/* Invoice Print Styles */ /* Invoice Print Styles */
@ -127,7 +130,7 @@
<div class="category-badge active" data-category="all">{% trans "All" %}</div> <div class="category-badge active" data-category="all">{% trans "All" %}</div>
{% for category in categories %} {% for category in categories %}
<div class="category-badge" data-category="{{ category.id }}"> <div class="category-badge" data-category="{{ category.id }}">
{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %} {{ category.name_ar }} / {{ category.name_en }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -145,7 +148,8 @@
{% endif %} {% endif %}
<div class="card-body p-1 text-center"> <div class="card-body p-1 text-center">
<div class="product-name fw-bold mb-1"> <div class="product-name fw-bold mb-1">
{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %} <div dir="rtl">{{ product.name_ar }}</div>
<div class="small text-muted fw-normal" style="font-size: 0.65rem;">{{ product.name_en }}</div>
</div> </div>
<p class="text-primary fw-bold mb-0" style="font-size: 0.8rem;">{{ site_settings.currency_symbol }}{{ product.price|floatformat:decimal_places }}</p> <p class="text-primary fw-bold mb-0" style="font-size: 0.8rem;">{{ site_settings.currency_symbol }}{{ product.price|floatformat:decimal_places }}</p>
<small class="text-muted d-block" style="font-size: 0.65rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{% trans "Stock" %}: {{ product.stock_quantity }}</small> <small class="text-muted d-block" style="font-size: 0.65rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{% trans "Stock" %}: {{ product.stock_quantity }}</small>
@ -161,9 +165,15 @@
<div class="card border-0 shadow-sm rounded-4 cart-container"> <div class="card border-0 shadow-sm rounded-4 cart-container">
<div class="card-header bg-white border-0 pt-4 px-4 d-flex justify-content-between align-items-center"> <div class="card-header bg-white border-0 pt-4 px-4 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0">{% trans "Current Order" %}</h5> <h5 class="fw-bold mb-0">{% trans "Current Order" %}</h5>
<button class="btn btn-sm btn-link text-danger text-decoration-none p-0" onclick="clearCart()"> <div class="d-flex gap-2 align-items-center">
<i class="bi bi-trash"></i> {% trans "Clear" %} <button class="btn btn-sm btn-outline-warning shadow-none position-relative" onclick="loadHeldSales()" data-bs-toggle="modal" data-bs-target="#heldSalesModal">
</button> <i class="bi bi-clock-history"></i>
<span id="heldCountBadge" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger d-none">0</span>
</button>
<button class="btn btn-sm btn-link text-danger text-decoration-none p-0" onclick="clearCart()">
<i class="bi bi-trash"></i> {% trans "Clear" %}
</button>
</div>
</div> </div>
<div class="px-4 mt-2"> <div class="px-4 mt-2">
@ -207,18 +217,18 @@
<span class="fw-bold fs-5 text-primary" id="totalAmount">{{ site_settings.currency_symbol }}0.000</span> <span class="fw-bold fs-5 text-primary" id="totalAmount">{{ site_settings.currency_symbol }}0.000</span>
</div> </div>
<div class="mb-3"> <div class="row g-2">
<label class="small text-muted mb-1">{% trans "Payment Method" %}</label> <div class="col-9">
<select id="paymentMethodSelect" class="form-select shadow-none"> <button id="payNowBtn" class="btn btn-primary w-100 py-3 fw-bold rounded-3 shadow-none" onclick="checkout()" disabled>
{% for method in payment_methods %} {% trans "PAY NOW" %}
<option value="{{ method.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ method.name_ar }}{% else %}{{ method.name_en }}{% endif %}</option> </button>
{% endfor %} </div>
</select> <div class="col-3">
<button id="holdBtn" class="btn btn-warning w-100 py-3 fw-bold rounded-3 shadow-none" onclick="holdSale()" disabled title="{% trans 'Hold Order' %}">
<i class="bi bi-pause-fill fs-4"></i>
</button>
</div>
</div> </div>
<button id="payNowBtn" class="btn btn-primary w-100 py-3 fw-bold rounded-3 shadow-none" onclick="checkout()" disabled>
{% trans "PAY NOW" %}
</button>
</div> </div>
</div> </div>
</div> </div>
@ -257,6 +267,109 @@
</div> </div>
</div> </div>
<!-- Held Sales Modal -->
<div class="modal fade no-print" id="heldSalesModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0 pb-0 px-4 pt-4">
<h5 class="fw-bold">{% trans "Held Sales" %}</h5>
<button type="button" class="btn-close shadow-none" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>{% trans "Time" %}</th>
<th>{% trans "Customer" %}</th>
<th>{% trans "Items" %}</th>
<th>{% trans "Total" %}</th>
<th class="text-end">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody id="heldSalesList">
<!-- Held sales injected here -->
</tbody>
</table>
</div>
<div id="noHeldSalesMsg" class="text-center py-5 text-muted d-none">
<i class="bi bi-inbox display-1 d-block mb-3 opacity-25"></i>
{% trans "No held sales found" %}
</div>
</div>
</div>
</div>
</div>
<!-- Payment Modal -->
<div class="modal fade no-print" id="paymentModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0 pb-0 px-4 pt-4">
<h5 class="fw-bold">{% trans "Complete Payment" %}</h5>
<button type="button" class="btn-close shadow-none" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="row g-4">
<!-- Left: Payment Types -->
<div class="col-md-3 border-end">
<label class="small fw-bold text-muted mb-2 d-block">{% trans "Payment Method" %}</label>
<div class="d-grid gap-2" id="paymentMethodButtons">
{% for method in payment_methods %}
<button class="btn btn-outline-primary payment-method-btn py-3 {% if forloop.first %}active{% endif %}"
data-id="{{ method.id }}" data-name-en="{{ method.name_en|lower }}"
onclick="selectPaymentMethod(this, '{{ method.id }}')">
{{ method.name_ar }}<br>
<small class="fw-normal">{{ method.name_en }}</small>
</button>
{% endfor %}
</div>
</div>
<!-- Middle: Details -->
<div class="col-md-5">
<div class="bg-light p-3 rounded-4 mb-3 text-center">
<label class="small text-muted d-block">{% trans "Total Payable" %}</label>
<h2 class="fw-bold mb-0 text-primary" id="modalTotalAmount">{{ site_settings.currency_symbol }}0.000</h2>
</div>
<div class="mb-3">
<label class="small fw-bold text-muted mb-1">{% trans "Cash Received" %}</label>
<input type="number" id="cashReceivedInput" class="form-control form-control-lg fw-bold text-center shadow-none"
placeholder="0.000" step="0.001" oninput="calculateBalance()">
</div>
<div class="bg-light p-3 rounded-4 text-center">
<label class="small text-muted d-block">{% trans "Balance / Change" %}</label>
<h2 class="fw-bold mb-0 text-success" id="balanceAmount">{{ site_settings.currency_symbol }}0.000</h2>
</div>
</div>
<!-- Right: Quick Cash Buttons -->
<div class="col-md-4">
<label class="small fw-bold text-muted mb-2 d-block">{% trans "Quick Cash" %}</label>
<div class="row g-2 mb-3">
<div class="col-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(1)">1</button></div>
<div class="col-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(5)">5</button></div>
<div class="col-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(10)">10</button></div>
<div class="col-6"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(20)">20</button></div>
<div class="col-12"><button class="btn btn-light w-100 py-3 fw-bold border" onclick="addCash(50)">50</button></div>
</div>
<button class="btn btn-secondary w-100 mb-2 py-2" onclick="setExactAmount()">{% trans "Exact Amount" %}</button>
<button class="btn btn-danger w-100 py-2" onclick="clearCash()">{% trans "Clear Cash" %}</button>
</div>
</div>
</div>
<div class="modal-footer border-0 px-4 pb-4">
<button type="button" class="btn btn-light rounded-3 px-4" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="button" id="confirmPaymentBtn" class="btn btn-primary rounded-3 px-5 py-2 fw-bold" onclick="processPayment()">
{% trans "CONFIRM & PRINT" %}
</button>
</div>
</div>
</div>
</div>
<!-- Invoice Template (Hidden) --> <!-- Invoice Template (Hidden) -->
<div id="invoice-print"> <div id="invoice-print">
<div class="invoice-header"> <div class="invoice-header">
@ -336,6 +449,7 @@
<script> <script>
let cart = []; let cart = [];
let lastSaleData = null; let lastSaleData = null;
let selectedPaymentMethodId = null;
const lang = '{{ LANGUAGE_CODE }}'; const lang = '{{ LANGUAGE_CODE }}';
const currency = '{{ site_settings.currency_symbol }}'; const currency = '{{ site_settings.currency_symbol }}';
const decimalPlaces = {{ decimal_places|default:3 }}; const decimalPlaces = {{ decimal_places|default:3 }};
@ -352,7 +466,6 @@
} else { } else {
cart.push({ cart.push({
id, id,
name: lang === 'ar' ? nameAr : nameEn,
name_en: nameEn, name_en: nameEn,
name_ar: nameAr, name_ar: nameAr,
price, price,
@ -376,9 +489,9 @@
renderCart(); renderCart();
} }
function clearCart() { function clearCart(confirmRequired = true) {
if (cart.length === 0) return; if (cart.length === 0) return;
if (confirm('{% trans "Are you sure you want to clear the current order?" %}')) { if (!confirmRequired || confirm('{% trans "Are you sure you want to clear the current order?" %}')) {
cart = []; cart = [];
document.getElementById('discountInput').value = 0; document.getElementById('discountInput').value = 0;
renderCart(); renderCart();
@ -398,24 +511,28 @@
const listContainer = document.getElementById('cartItemsList'); const listContainer = document.getElementById('cartItemsList');
const emptyMsg = document.getElementById('emptyCartMsg'); const emptyMsg = document.getElementById('emptyCartMsg');
const payBtn = document.getElementById('payNowBtn'); const payBtn = document.getElementById('payNowBtn');
const holdBtn = document.getElementById('holdBtn');
if (cart.length === 0) { if (cart.length === 0) {
emptyMsg.classList.remove('d-none'); emptyMsg.classList.remove('d-none');
listContainer.innerHTML = ''; listContainer.innerHTML = '';
updateTotals(); updateTotals();
payBtn.disabled = true; payBtn.disabled = true;
holdBtn.disabled = true;
return; return;
} }
emptyMsg.classList.add('d-none'); emptyMsg.classList.add('d-none');
payBtn.disabled = false; payBtn.disabled = false;
holdBtn.disabled = false;
let html = ''; let html = '';
cart.forEach(item => { cart.forEach(item => {
html += ` html += `
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<div style="flex: 1;"> <div style="flex: 1;">
<div class="fw-bold small">${item.name}</div> <div class="fw-bold small" dir="rtl">${item.name_ar}</div>
<div class="text-muted" style="font-size: 0.7rem;">${item.name_en}</div>
<div class="text-muted small">${currency} ${formatAmount(item.price)} x ${item.quantity}</div> <div class="text-muted small">${currency} ${formatAmount(item.price)} x ${item.quantity}</div>
</div> </div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
@ -433,11 +550,74 @@
function checkout() { function checkout() {
if (cart.length === 0) return; if (cart.length === 0) return;
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
const totalAmount = Math.max(0, subtotal - discount);
const payBtn = document.getElementById('payNowBtn'); document.getElementById('modalTotalAmount').innerText = `${currency} ${formatAmount(totalAmount)}`;
const originalText = payBtn.innerText; document.getElementById('cashReceivedInput').value = '';
payBtn.disabled = true; document.getElementById('balanceAmount').innerText = `${currency} ${formatAmount(0)}`;
payBtn.innerText = '{% trans "Processing..." %}';
// Default to first payment method
const firstBtn = document.querySelector('.payment-method-btn');
if (firstBtn) {
selectPaymentMethod(firstBtn, firstBtn.dataset.id);
}
const paymentModal = new bootstrap.Modal(document.getElementById('paymentModal'));
paymentModal.show();
}
function selectPaymentMethod(btn, id) {
document.querySelectorAll('.payment-method-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedPaymentMethodId = id;
// Auto-fill exact amount for Card and Bank Transfer
const nameEn = btn.dataset.nameEn ? btn.dataset.nameEn.toLowerCase() : '';
if (nameEn.includes('card') || nameEn.includes('transfer') || nameEn.includes('bank')) {
setExactAmount();
}
}
function addCash(amount) {
const input = document.getElementById('cashReceivedInput');
const current = parseFloat(input.value) || 0;
input.value = (current + amount).toFixed(decimalPlaces);
calculateBalance();
}
function setExactAmount() {
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
const totalAmount = Math.max(0, subtotal - discount);
document.getElementById('cashReceivedInput').value = totalAmount.toFixed(decimalPlaces);
calculateBalance();
}
function clearCash() {
document.getElementById('cashReceivedInput').value = '';
calculateBalance();
}
function calculateBalance() {
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
const totalAmount = Math.max(0, subtotal - discount);
const received = parseFloat(document.getElementById('cashReceivedInput').value) || 0;
const balance = Math.max(0, received - totalAmount);
document.getElementById('balanceAmount').innerText = `${currency} ${formatAmount(balance)}`;
}
function processPayment() {
const confirmBtn = document.getElementById('confirmPaymentBtn');
const originalText = confirmBtn.innerText;
confirmBtn.disabled = true;
confirmBtn.innerText = '{% trans "Processing..." %}';
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
const discount = parseFloat(document.getElementById('discountInput').value) || 0; const discount = parseFloat(document.getElementById('discountInput').value) || 0;
@ -445,7 +625,7 @@
const data = { const data = {
customer_id: document.getElementById('customerSelect').value, customer_id: document.getElementById('customerSelect').value,
payment_method_id: document.getElementById('paymentMethodSelect').value, payment_method_id: selectedPaymentMethodId,
items: cart, items: cart,
total_amount: totalAmount, total_amount: totalAmount,
paid_amount: totalAmount, paid_amount: totalAmount,
@ -471,11 +651,20 @@
if (data.success) { if (data.success) {
lastSaleData = data; lastSaleData = data;
prepareInvoice(data); prepareInvoice(data);
// Hide payment modal
const pModalElement = document.getElementById('paymentModal');
const pModalInstance = bootstrap.Modal.getInstance(pModalElement);
if (pModalInstance) pModalInstance.hide();
cart = []; cart = [];
document.getElementById('discountInput').value = 0; document.getElementById('discountInput').value = 0;
renderCart(); renderCart();
// Show receipt modal
const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal')); const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
receiptModal.show(); receiptModal.show();
updateHeldCount();
} else { } else {
alert('Error: ' + data.error); alert('Error: ' + data.error);
} }
@ -485,8 +674,8 @@
alert('Checkout failed: ' + (error.error || error.message || 'Unknown error')); alert('Checkout failed: ' + (error.error || error.message || 'Unknown error'));
}) })
.finally(() => { .finally(() => {
payBtn.disabled = cart.length === 0; confirmBtn.disabled = false;
payBtn.innerText = originalText; confirmBtn.innerText = originalText;
}); });
} }
@ -516,8 +705,8 @@
itemsHtml += ` itemsHtml += `
<tr> <tr>
<td> <td>
<div>${item.name_en}</div> <div class="rtl">${item.name_ar}</div>
<div class="rtl text-muted" style="font-size: 9px;">${item.name_ar}</div> <div class="text-muted" style="font-size: 9px;">${item.name_en}</div>
</td> </td>
<td style="text-align: center;">${item.qty}</td> <td style="text-align: center;">${item.qty}</td>
<td style="text-align: right;">${data.business.currency} ${formatAmount(item.total)}</td> <td style="text-align: right;">${data.business.currency} ${formatAmount(item.total)}</td>
@ -554,7 +743,7 @@
.then(data => { .then(data => {
if (data.success) { if (data.success) {
const select = document.getElementById('customerSelect'); const select = document.getElementById('customerSelect');
const option = new Option(data.customer.name, data.customer.id, true, true); const option = new Option(data.name, data.id, true, true);
select.add(option); select.add(option);
// Close modal // Close modal
@ -568,6 +757,144 @@
.catch(error => console.error('Error:', error)); .catch(error => console.error('Error:', error));
} }
// Held Sales Logic
function holdSale() {
if (cart.length === 0) return;
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
const totalAmount = Math.max(0, subtotal - discount);
const data = {
customer_id: document.getElementById('customerSelect').value,
items: cart,
total_amount: totalAmount,
notes: ""
};
fetch('{% url "hold_sale_api" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
clearCart(false);
updateHeldCount();
} else {
alert('Error holding sale: ' + data.error);
}
})
.catch(error => console.error('Error:', error));
}
function updateHeldCount() {
fetch('{% url "get_held_sales_api" %}')
.then(response => response.json())
.then(data => {
if (data.success) {
const badge = document.getElementById('heldCountBadge');
if (data.held_sales.length > 0) {
badge.innerText = data.held_sales.length;
badge.classList.remove('d-none');
} else {
badge.classList.add('d-none');
}
}
});
}
function loadHeldSales() {
const listContainer = document.getElementById('heldSalesList');
const emptyMsg = document.getElementById('noHeldSalesMsg');
listContainer.innerHTML = '<tr><td colspan="5" class="text-center">{% trans "Loading..." %}</td></tr>';
fetch('{% url "get_held_sales_api" %}')
.then(response => response.json())
.then(data => {
if (data.success) {
if (data.held_sales.length === 0) {
listContainer.innerHTML = '';
emptyMsg.classList.remove('d-none');
return;
}
emptyMsg.classList.add('d-none');
let html = '';
data.held_sales.forEach(sale => {
html += `
<tr>
<td class="small">${sale.created_at}</td>
<td class="fw-bold">${sale.customer_name}</td>
<td><span class="badge bg-secondary rounded-pill">${sale.items_count}</span></td>
<td class="fw-bold text-primary">${currency} ${formatAmount(sale.total_amount)}</td>
<td class="text-end">
<button class="btn btn-sm btn-primary rounded-3 px-3 me-1" onclick="recallHeldSale(${sale.id})">
<i class="bi bi-arrow-repeat me-1"></i> {% trans "Recall" %}
</button>
<button class="btn btn-sm btn-outline-danger rounded-3" onclick="deleteHeldSale(${sale.id})">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`;
});
listContainer.innerHTML = html;
}
})
.catch(error => console.error('Error:', error));
}
function recallHeldSale(id) {
if (cart.length > 0) {
if (!confirm('{% trans "The current cart is not empty. Recalling a held sale will clear current items. Continue?" %}')) {
return;
}
}
fetch(`/api/recall-held-sale/${id}/`)
.then(response => response.json())
.then(data => {
if (data.success) {
cart = data.items;
document.getElementById('customerSelect').value = data.customer_id || "";
renderCart();
updateHeldCount();
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('heldSalesModal'));
modal.hide();
} else {
alert('Error recalling sale: ' + data.error);
}
})
.catch(error => console.error('Error:', error));
}
function deleteHeldSale(id) {
if (!confirm('{% trans "Are you sure you want to delete this held sale?" %}')) return;
fetch(`/api/delete-held-sale/${id}/`, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadHeldSales();
updateHeldCount();
}
});
}
// Initial load
updateHeldCount();
// Search and Category Filtering // Search and Category Filtering
document.getElementById('productSearch').addEventListener('input', filterProducts); document.getElementById('productSearch').addEventListener('input', filterProducts);
document.querySelectorAll('.category-badge').forEach(badge => { document.querySelectorAll('.category-badge').forEach(badge => {

View File

@ -116,8 +116,8 @@
{% for item in purchase.items.all %} {% for item in purchase.items.all %}
<tr> <tr>
<td class="py-3 ps-4"> <td class="py-3 ps-4">
<div class="fw-bold">{{ item.product.name_en }}</div> <div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
<div class="text-muted small">{{ item.product.name_ar }}</div> <div class="text-muted small">{{ item.product.name_en }}</div>
</td> </td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }}</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-center">{{ item.quantity }}</td>
@ -175,7 +175,7 @@
<td>{{ payment.payment_date|date:"Y-m-d" }}</td> <td>{{ payment.payment_date|date:"Y-m-d" }}</td>
<td> <td>
{% if payment.payment_method %} {% if payment.payment_method %}
{% if LANGUAGE_CODE == 'ar' %}{{ payment.payment_method.name_ar }}{% else %}{{ payment.payment_method.name_en }}{% endif %} {{ payment.payment_method.name_ar }} / {{ payment.payment_method.name_en }}
{% else %} {% else %}
{{ payment.payment_method_name }} {{ payment.payment_method_name }}
{% endif %} {% endif %}

View File

@ -94,8 +94,8 @@
{% for item in purchase_return.items.all %} {% for item in purchase_return.items.all %}
<tr> <tr>
<td class="py-3 ps-4"> <td class="py-3 ps-4">
<div class="fw-bold">{{ item.product.name_en }}</div> <div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
<div class="text-muted small">{{ item.product.name_ar }}</div> <div class="text-muted small">{{ item.product.name_en }}</div>
</td> </td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }}</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-center">{{ item.quantity }}</td>

View File

@ -138,8 +138,8 @@
{% for item in quotation.items.all %} {% for item in quotation.items.all %}
<tr> <tr>
<td class="py-3 ps-4"> <td class="py-3 ps-4">
<div class="fw-bold">{{ item.product.name_en }}</div> <div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
<div class="text-muted small">{{ item.product.name_ar }}</div> <div class="text-muted small">{{ item.product.name_en }}</div>
</td> </td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}</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-center">{{ item.quantity }}</td>

View File

@ -94,8 +94,8 @@
{% for item in sale_return.items.all %} {% for item in sale_return.items.all %}
<tr> <tr>
<td class="py-3 ps-4"> <td class="py-3 ps-4">
<div class="fw-bold">{{ item.product.name_en }}</div> <div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
<div class="text-muted small">{{ item.product.name_ar }}</div> <div class="text-muted small">{{ item.product.name_en }}</div>
</td> </td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}</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-center">{{ item.quantity }}</td>

View File

@ -11,6 +11,7 @@ urlpatterns = [
path('reports/', views.reports, name='reports'), path('reports/', views.reports, name='reports'),
path('settings/', views.settings_view, name='settings'), path('settings/', views.settings_view, name='settings'),
path('users/', views.user_management, name='user_management'), path('users/', views.user_management, name='user_management'),
path('api/group-details/<int:pk>/', views.group_details_api, name='group_details_api'),
# Invoices (Sales) # Invoices (Sales)
path('invoices/', views.invoice_list, name='invoices'), path('invoices/', views.invoice_list, name='invoices'),
@ -51,6 +52,12 @@ urlpatterns = [
path('api/create-sale/', views.create_sale_api, name='create_sale_api'), path('api/create-sale/', views.create_sale_api, name='create_sale_api'),
path('api/create-purchase/', views.create_purchase_api, name='create_purchase_api'), path('api/create-purchase/', views.create_purchase_api, name='create_purchase_api'),
# POS Held Sales
path('api/hold-sale/', views.hold_sale_api, name='hold_sale_api'),
path('api/held-sales/', views.get_held_sales_api, name='get_held_sales_api'),
path('api/recall-held-sale/<int:pk>/', views.recall_held_sale_api, name='recall_held_sale_api'),
path('api/delete-held-sale/<int:pk>/', views.delete_held_sale_api, name='delete_held_sale_api'),
# Customers # Customers
path('customers/add/', views.add_customer, name='add_customer'), path('customers/add/', views.add_customer, name='add_customer'),
path('customers/edit/<int:pk>/', views.edit_customer, name='edit_customer'), path('customers/edit/<int:pk>/', views.edit_customer, name='edit_customer'),
@ -88,4 +95,4 @@ urlpatterns = [
path('settings/payment-methods/edit/<int:pk>/', views.edit_payment_method, name='edit_payment_method'), path('settings/payment-methods/edit/<int:pk>/', views.edit_payment_method, name='edit_payment_method'),
path('settings/payment-methods/delete/<int:pk>/', views.delete_payment_method, name='delete_payment_method'), path('settings/payment-methods/delete/<int:pk>/', views.delete_payment_method, name='delete_payment_method'),
path('api/add-payment-method-ajax/', views.add_payment_method_ajax, name='add_payment_method_ajax'), path('api/add-payment-method-ajax/', views.add_payment_method_ajax, name='add_payment_method_ajax'),
] ]

View File

@ -14,7 +14,7 @@ from .models import (
SaleItem, SalePayment, SystemSetting, SaleItem, SalePayment, SystemSetting,
Quotation, QuotationItem, Quotation, QuotationItem,
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem,
PaymentMethod PaymentMethod, HeldSale
) )
import json import json
from datetime import timedelta from datetime import timedelta
@ -857,10 +857,10 @@ def delete_supplier(request, pk):
@login_required @login_required
def suggest_sku(request): def suggest_sku(request):
""" """
API endpoint to suggest a unique SKU. API endpoint to suggest a unique SKU.
""" """
while True: while True:
# Generate a random 8-digit number # Generate a random 8-digit number
sku = "".join(random.choices(string.digits, k=8)) sku = "".join(random.choices(string.digits, k=8))
@ -1314,3 +1314,66 @@ def add_customer_ajax(request):
except Exception as e: except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@csrf_exempt
@login_required
def hold_sale_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
customer_id = data.get('customer_id')
cart_data = data.get('items', [])
total_amount = data.get('total_amount', 0)
notes = data.get('notes', '')
customer = None
if customer_id:
customer = Customer.objects.filter(id=customer_id).first()
held_sale = HeldSale.objects.create(
customer=customer,
cart_data=cart_data,
total_amount=total_amount,
notes=notes,
created_by=request.user
)
return JsonResponse({'success': True, 'held_id': held_sale.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def get_held_sales_api(request):
held_sales = HeldSale.objects.filter(created_by=request.user).select_related('customer').order_by('-created_at')
data = []
for hs in held_sales:
data.append({
'id': hs.id,
'customer_name': hs.customer.name if hs.customer else 'Guest',
'total_amount': float(hs.total_amount),
'items_count': len(hs.cart_data),
'created_at': hs.created_at.strftime("%Y-%m-%d %H:%M"),
'notes': hs.notes
})
return JsonResponse({'success': True, 'held_sales': data})
@csrf_exempt
@login_required
def recall_held_sale_api(request, pk):
held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
data = {
'success': True,
'customer_id': held_sale.customer.id if held_sale.customer else None,
'items': held_sale.cart_data,
'total_amount': float(held_sale.total_amount),
'notes': held_sale.notes
}
held_sale.delete()
return JsonResponse(data)
@csrf_exempt
@login_required
def delete_held_sale_api(request, pk):
held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
held_sale.delete()
return JsonResponse({'success': True})