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):
|
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
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 => {
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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})
|
||||||
Loading…
x
Reference in New Issue
Block a user