Autosave: 20260202-164535
This commit is contained in:
parent
e9c5a5c213
commit
0ae32328a7
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
core/migrations/0013_heldsale.py
Normal file
28
core/migrations/0013_heldsale.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0013_heldsale.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0013_heldsale.cpython-311.pyc
Normal file
Binary file not shown.
@ -268,6 +268,17 @@ class PurchaseReturnItem(models.Model):
|
||||
def __str__(self):
|
||||
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):
|
||||
business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting")
|
||||
address = models.TextField(_("Address"), blank=True)
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
<select class="form-select">
|
||||
<option value="">{% trans "All Categories" %}</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option>
|
||||
<option value="{{ category.id }}">{{ category.name_ar }} / {{ category.name_en }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@ -118,13 +118,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="fw-bold text-dark">{% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %}</div>
|
||||
<small class="text-muted">{{ product.unit.name_en|default:"" }}</small>
|
||||
<div class="fw-bold text-dark" dir="rtl">{{ product.name_ar }}</div>
|
||||
<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>
|
||||
</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>
|
||||
<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:"" }}
|
||||
@ -348,7 +349,7 @@
|
||||
<select name="category" id="categorySelect" class="form-select rounded-3" required>
|
||||
<option value="">{% trans "Select Category" %}</option>
|
||||
{% 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 %}
|
||||
</select>
|
||||
</div>
|
||||
@ -357,7 +358,7 @@
|
||||
<select name="unit" id="unitSelect" class="form-select rounded-3">
|
||||
<option value="">{% trans "Select Unit" %}</option>
|
||||
{% for unit in units %}
|
||||
<option value="{{ unit.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ unit.name_ar }}{% else %}{{ unit.name_en }}{% endif %}</option>
|
||||
<option value="{{ unit.id }}">{{ unit.name_ar }} / {{ unit.name_en }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -112,8 +112,8 @@
|
||||
{% for item in sale.items.all %}
|
||||
<tr>
|
||||
<td class="py-3 ps-4">
|
||||
<div class="fw-bold">{{ item.product.name_en }}</div>
|
||||
<div class="text-muted small">{{ item.product.name_ar }}</div>
|
||||
<div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
|
||||
<div class="text-muted small">{{ item.product.name_en }}</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>
|
||||
@ -189,7 +189,7 @@
|
||||
<td>{{ payment.payment_date|date:"Y-m-d" }}</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
{{ payment.payment_method_name }}
|
||||
{% endif %}
|
||||
|
||||
@ -48,11 +48,14 @@
|
||||
.product-name {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
height: 2.4em;
|
||||
height: auto;
|
||||
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 */
|
||||
@ -127,7 +130,7 @@
|
||||
<div class="category-badge active" data-category="all">{% trans "All" %}</div>
|
||||
{% for category in categories %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -145,7 +148,8 @@
|
||||
{% endif %}
|
||||
<div class="card-body p-1 text-center">
|
||||
<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>
|
||||
<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>
|
||||
@ -161,9 +165,15 @@
|
||||
<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">
|
||||
<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()">
|
||||
<i class="bi bi-trash"></i> {% trans "Clear" %}
|
||||
</button>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<button class="btn btn-sm btn-outline-warning shadow-none position-relative" onclick="loadHeldSales()" data-bs-toggle="modal" data-bs-target="#heldSalesModal">
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="small text-muted mb-1">{% trans "Payment Method" %}</label>
|
||||
<select id="paymentMethodSelect" class="form-select shadow-none">
|
||||
{% for method in payment_methods %}
|
||||
<option value="{{ method.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ method.name_ar }}{% else %}{{ method.name_en }}{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="row g-2">
|
||||
<div class="col-9">
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
@ -257,6 +267,109 @@
|
||||
</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) -->
|
||||
<div id="invoice-print">
|
||||
<div class="invoice-header">
|
||||
@ -336,6 +449,7 @@
|
||||
<script>
|
||||
let cart = [];
|
||||
let lastSaleData = null;
|
||||
let selectedPaymentMethodId = null;
|
||||
const lang = '{{ LANGUAGE_CODE }}';
|
||||
const currency = '{{ site_settings.currency_symbol }}';
|
||||
const decimalPlaces = {{ decimal_places|default:3 }};
|
||||
@ -352,7 +466,6 @@
|
||||
} else {
|
||||
cart.push({
|
||||
id,
|
||||
name: lang === 'ar' ? nameAr : nameEn,
|
||||
name_en: nameEn,
|
||||
name_ar: nameAr,
|
||||
price,
|
||||
@ -376,9 +489,9 @@
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function clearCart() {
|
||||
function clearCart(confirmRequired = true) {
|
||||
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 = [];
|
||||
document.getElementById('discountInput').value = 0;
|
||||
renderCart();
|
||||
@ -398,24 +511,28 @@
|
||||
const listContainer = document.getElementById('cartItemsList');
|
||||
const emptyMsg = document.getElementById('emptyCartMsg');
|
||||
const payBtn = document.getElementById('payNowBtn');
|
||||
const holdBtn = document.getElementById('holdBtn');
|
||||
|
||||
if (cart.length === 0) {
|
||||
emptyMsg.classList.remove('d-none');
|
||||
listContainer.innerHTML = '';
|
||||
updateTotals();
|
||||
payBtn.disabled = true;
|
||||
holdBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
emptyMsg.classList.add('d-none');
|
||||
payBtn.disabled = false;
|
||||
holdBtn.disabled = false;
|
||||
let html = '';
|
||||
|
||||
cart.forEach(item => {
|
||||
html += `
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<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>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@ -434,10 +551,73 @@
|
||||
function checkout() {
|
||||
if (cart.length === 0) return;
|
||||
|
||||
const payBtn = document.getElementById('payNowBtn');
|
||||
const originalText = payBtn.innerText;
|
||||
payBtn.disabled = true;
|
||||
payBtn.innerText = '{% trans "Processing..." %}';
|
||||
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('modalTotalAmount').innerText = `${currency} ${formatAmount(totalAmount)}`;
|
||||
document.getElementById('cashReceivedInput').value = '';
|
||||
document.getElementById('balanceAmount').innerText = `${currency} ${formatAmount(0)}`;
|
||||
|
||||
// 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 discount = parseFloat(document.getElementById('discountInput').value) || 0;
|
||||
@ -445,7 +625,7 @@
|
||||
|
||||
const data = {
|
||||
customer_id: document.getElementById('customerSelect').value,
|
||||
payment_method_id: document.getElementById('paymentMethodSelect').value,
|
||||
payment_method_id: selectedPaymentMethodId,
|
||||
items: cart,
|
||||
total_amount: totalAmount,
|
||||
paid_amount: totalAmount,
|
||||
@ -471,11 +651,20 @@
|
||||
if (data.success) {
|
||||
lastSaleData = data;
|
||||
prepareInvoice(data);
|
||||
|
||||
// Hide payment modal
|
||||
const pModalElement = document.getElementById('paymentModal');
|
||||
const pModalInstance = bootstrap.Modal.getInstance(pModalElement);
|
||||
if (pModalInstance) pModalInstance.hide();
|
||||
|
||||
cart = [];
|
||||
document.getElementById('discountInput').value = 0;
|
||||
renderCart();
|
||||
|
||||
// Show receipt modal
|
||||
const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
|
||||
receiptModal.show();
|
||||
updateHeldCount();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
@ -485,8 +674,8 @@
|
||||
alert('Checkout failed: ' + (error.error || error.message || 'Unknown error'));
|
||||
})
|
||||
.finally(() => {
|
||||
payBtn.disabled = cart.length === 0;
|
||||
payBtn.innerText = originalText;
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.innerText = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
@ -516,8 +705,8 @@
|
||||
itemsHtml += `
|
||||
<tr>
|
||||
<td>
|
||||
<div>${item.name_en}</div>
|
||||
<div class="rtl text-muted" style="font-size: 9px;">${item.name_ar}</div>
|
||||
<div class="rtl">${item.name_ar}</div>
|
||||
<div class="text-muted" style="font-size: 9px;">${item.name_en}</div>
|
||||
</td>
|
||||
<td style="text-align: center;">${item.qty}</td>
|
||||
<td style="text-align: right;">${data.business.currency} ${formatAmount(item.total)}</td>
|
||||
@ -554,7 +743,7 @@
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
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);
|
||||
|
||||
// Close modal
|
||||
@ -568,6 +757,144 @@
|
||||
.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
|
||||
document.getElementById('productSearch').addEventListener('input', filterProducts);
|
||||
document.querySelectorAll('.category-badge').forEach(badge => {
|
||||
|
||||
@ -116,8 +116,8 @@
|
||||
{% for item in purchase.items.all %}
|
||||
<tr>
|
||||
<td class="py-3 ps-4">
|
||||
<div class="fw-bold">{{ item.product.name_en }}</div>
|
||||
<div class="text-muted small">{{ item.product.name_ar }}</div>
|
||||
<div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
|
||||
<div class="text-muted small">{{ item.product.name_en }}</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>
|
||||
@ -175,7 +175,7 @@
|
||||
<td>{{ payment.payment_date|date:"Y-m-d" }}</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
{{ payment.payment_method_name }}
|
||||
{% endif %}
|
||||
|
||||
@ -94,8 +94,8 @@
|
||||
{% 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>
|
||||
<div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
|
||||
<div class="text-muted small">{{ item.product.name_en }}</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>
|
||||
|
||||
@ -138,8 +138,8 @@
|
||||
{% 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>
|
||||
<div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
|
||||
<div class="text-muted small">{{ item.product.name_en }}</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>
|
||||
|
||||
@ -94,8 +94,8 @@
|
||||
{% 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>
|
||||
<div class="fw-bold" dir="rtl">{{ item.product.name_ar }}</div>
|
||||
<div class="text-muted small">{{ item.product.name_en }}</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>
|
||||
|
||||
@ -11,6 +11,7 @@ urlpatterns = [
|
||||
path('reports/', views.reports, name='reports'),
|
||||
path('settings/', views.settings_view, name='settings'),
|
||||
path('users/', views.user_management, name='user_management'),
|
||||
path('api/group-details/<int:pk>/', views.group_details_api, name='group_details_api'),
|
||||
|
||||
# Invoices (Sales)
|
||||
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-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
|
||||
path('customers/add/', views.add_customer, name='add_customer'),
|
||||
path('customers/edit/<int:pk>/', views.edit_customer, name='edit_customer'),
|
||||
|
||||
@ -14,7 +14,7 @@ from .models import (
|
||||
SaleItem, SalePayment, SystemSetting,
|
||||
Quotation, QuotationItem,
|
||||
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem,
|
||||
PaymentMethod
|
||||
PaymentMethod, HeldSale
|
||||
)
|
||||
import json
|
||||
from datetime import timedelta
|
||||
@ -1314,3 +1314,66 @@ def add_customer_ajax(request):
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||
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})
|
||||
Loading…
x
Reference in New Issue
Block a user