Autosave: 20260203-052143
This commit is contained in:
parent
573f45e183
commit
f19ade40ee
Binary file not shown.
@ -1,3 +1,4 @@
|
|||||||
|
from django.utils import timezone
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from core.models import Sale, SalePayment, Purchase, PurchasePayment, Expense, SaleReturn, PurchaseReturn
|
from core.models import Sale, SalePayment, Purchase, PurchasePayment, Expense, SaleReturn, PurchaseReturn
|
||||||
|
|||||||
BIN
assets/pasted-20260203-040015-70bc78c5.jpg
Normal file
BIN
assets/pasted-20260203-040015-70bc78c5.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/vm-shot-2026-02-03T03-59-49-172Z.jpg
Normal file
BIN
assets/vm-shot-2026-02-03T03-59-49-172Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
@ -26,6 +26,8 @@ ALLOWED_HOSTS = [
|
|||||||
os.getenv("HOST_FQDN", ""),
|
os.getenv("HOST_FQDN", ""),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
origin for origin in [
|
origin for origin in [
|
||||||
os.getenv("HOST_FQDN", ""),
|
os.getenv("HOST_FQDN", ""),
|
||||||
@ -37,11 +39,13 @@ CSRF_TRUSTED_ORIGINS = [
|
|||||||
for host in CSRF_TRUSTED_ORIGINS
|
for host in CSRF_TRUSTED_ORIGINS
|
||||||
]
|
]
|
||||||
|
|
||||||
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
|
# Cookies must always be HTTPS-only; SameSite=None is required for iframes.
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
SESSION_COOKIE_SAMESITE = "None"
|
SESSION_COOKIE_SAMESITE = "None"
|
||||||
CSRF_COOKIE_SAMESITE = "None"
|
CSRF_COOKIE_SAMESITE = "None"
|
||||||
|
LANGUAGE_COOKIE_SECURE = True
|
||||||
|
LANGUAGE_COOKIE_SAMESITE = "None"
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||||
@ -85,6 +89,7 @@ TEMPLATES = [
|
|||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'django.template.context_processors.i18n',
|
||||||
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
||||||
'core.context_processors.project_context',
|
'core.context_processors.project_context',
|
||||||
'core.context_processors.global_settings',
|
'core.context_processors.global_settings',
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-03 05:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0017_expensecategory_accounting_account'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='wablas_enabled',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Enable WhatsApp Gateway'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='wablas_server_url',
|
||||||
|
field=models.URLField(blank=True, help_text='Example: https://console.wablas.com', verbose_name='Wablas Server URL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='systemsetting',
|
||||||
|
name='wablas_token',
|
||||||
|
field=models.CharField(blank=True, max_length=255, verbose_name='Wablas API Token'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -361,6 +361,11 @@ class SystemSetting(models.Model):
|
|||||||
currency_per_point = models.DecimalField(_("Currency Value per Point"), max_digits=10, decimal_places=3, default=0.010)
|
currency_per_point = models.DecimalField(_("Currency Value per Point"), max_digits=10, decimal_places=3, default=0.010)
|
||||||
min_points_to_redeem = models.PositiveIntegerField(_("Minimum Points to Redeem"), default=100)
|
min_points_to_redeem = models.PositiveIntegerField(_("Minimum Points to Redeem"), default=100)
|
||||||
|
|
||||||
|
# WhatsApp (Wablas) Settings
|
||||||
|
wablas_enabled = models.BooleanField(_("Enable WhatsApp Gateway"), default=False)
|
||||||
|
wablas_token = models.CharField(_("Wablas API Token"), max_length=255, blank=True)
|
||||||
|
wablas_server_url = models.URLField(_("Wablas Server URL"), blank=True, help_text="Example: https://console.wablas.com")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.business_name
|
return self.business_name
|
||||||
|
|
||||||
|
|||||||
@ -386,6 +386,27 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
// Prevent double form submission
|
||||||
|
document.addEventListener('submit', function (e) {
|
||||||
|
const form = e.target;
|
||||||
|
if (form.tagName === 'FORM') {
|
||||||
|
if (form.getAttribute('data-submitted') === 'true') {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form.setAttribute('data-submitted', 'true');
|
||||||
|
const submitButtons = form.querySelectorAll('button[type="submit"], input[type="submit"]');
|
||||||
|
submitButtons.forEach(btn => {
|
||||||
|
btn.disabled = true;
|
||||||
|
// Add spinner if it is a button and not already there
|
||||||
|
if (btn.tagName === 'BUTTON' && !btn.querySelector('.spinner-border')) {
|
||||||
|
const originalText = btn.innerHTML;
|
||||||
|
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${originalText}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n l10n %}
|
||||||
|
|
||||||
{% block title %}{% trans "New Invoice" %} | {{ site_settings.business_name }}{% endblock %}
|
{% block title %}{% trans "New Invoice" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
|
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
||||||
</td>
|
</td>
|
||||||
@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">{% trans "Subtotal" %}</span>
|
<span class="text-muted">{% trans "Subtotal" %}</span>
|
||||||
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</span>
|
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
<div class="d-flex justify-content-between mb-4">
|
||||||
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
||||||
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(3) ]]</h4>
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(decimalPlaces) ]]</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Details -->
|
<!-- Payment Details -->
|
||||||
@ -168,6 +168,7 @@
|
|||||||
|
|
||||||
<!-- Vue.js 3 -->
|
<!-- Vue.js 3 -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
{% localize off %}
|
||||||
<script>
|
<script>
|
||||||
const { createApp } = Vue;
|
const { createApp } = Vue;
|
||||||
|
|
||||||
@ -178,12 +179,12 @@
|
|||||||
products: [
|
products: [
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
{
|
{
|
||||||
id: {{ p.id }},
|
id: {{ p.id|default:0 }},
|
||||||
name_en: "{{ p.name_en }}",
|
name_en: "{{ p.name_en|escapejs }}",
|
||||||
name_ar: "{{ p.name_ar }}",
|
name_ar: "{{ p.name_ar|escapejs }}",
|
||||||
sku: "{{ p.sku }}",
|
sku: "{{ p.sku|escapejs }}",
|
||||||
price: {{ p.price }},
|
price: {{ p.price|default:0 }},
|
||||||
stock: {{ p.stock_quantity }}
|
stock: {{ p.stock_quantity|default:0 }}
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
],
|
],
|
||||||
@ -198,7 +199,8 @@
|
|||||||
discount: 0,
|
discount: 0,
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
currencySymbol: '{{ site_settings.currency_symbol }}',
|
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
|
||||||
|
decimalPlaces: {{ decimal_places|default:3 }},
|
||||||
isProcessing: false
|
isProcessing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -243,6 +245,7 @@
|
|||||||
this.cart.splice(index, 1);
|
this.cart.splice(index, 1);
|
||||||
},
|
},
|
||||||
saveSale() {
|
saveSale() {
|
||||||
|
if (this.isProcessing) return;
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
|
|
||||||
let actualPaidAmount = 0;
|
let actualPaidAmount = 0;
|
||||||
@ -274,6 +277,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
@ -295,4 +299,5 @@
|
|||||||
}
|
}
|
||||||
}).mount('#saleApp');
|
}).mount('#saleApp');
|
||||||
</script>
|
</script>
|
||||||
|
{% endlocalize %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n l10n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Edit Invoice" %} | {{ site_settings.business_name }}{% endblock %}
|
{% block title %}{% trans "Edit Invoice" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
@ -82,7 +82,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
|
<td class="text-end fw-bold">[[ currencySymbol ]][[ (parseFloat(item.price) * parseFloat(item.quantity)).toFixed(decimalPlaces) ]]</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
||||||
</td>
|
</td>
|
||||||
@ -107,7 +107,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">{% trans "Subtotal" %}</span>
|
<span class="text-muted">{% trans "Subtotal" %}</span>
|
||||||
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</span>
|
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
<div class="d-flex justify-content-between mb-4">
|
||||||
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
||||||
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(3) ]]</h4>
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(decimalPlaces) ]]</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Details -->
|
<!-- Payment Details -->
|
||||||
@ -173,6 +173,7 @@
|
|||||||
|
|
||||||
<!-- Vue.js 3 -->
|
<!-- Vue.js 3 -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
{% localize off %}
|
||||||
<script>
|
<script>
|
||||||
const { createApp } = Vue;
|
const { createApp } = Vue;
|
||||||
|
|
||||||
@ -183,12 +184,12 @@
|
|||||||
products: [
|
products: [
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
{
|
{
|
||||||
id: {{ p.id }},
|
id: {{ p.id|default:0 }},
|
||||||
name_en: "{{ p.name_en }}",
|
name_en: "{{ p.name_en|escapejs }}",
|
||||||
name_ar: "{{ p.name_ar }}",
|
name_ar: "{{ p.name_ar|escapejs }}",
|
||||||
sku: "{{ p.sku }}",
|
sku: "{{ p.sku|escapejs }}",
|
||||||
price: {{ p.price }},
|
price: {{ p.price|default:0 }},
|
||||||
stock: {{ p.stock_quantity }}
|
stock: {{ p.stock_quantity|default:0 }}
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
],
|
],
|
||||||
@ -196,14 +197,15 @@
|
|||||||
filteredProducts: [],
|
filteredProducts: [],
|
||||||
cart: {{ cart_json|safe }},
|
cart: {{ cart_json|safe }},
|
||||||
customerId: '{{ sale.customer_id|default:"" }}',
|
customerId: '{{ sale.customer_id|default:"" }}',
|
||||||
invoiceNumber: '{{ sale.invoice_number|default:"" }}',
|
invoiceNumber: '{{ sale.invoice_number|escapejs|default:"" }}',
|
||||||
paymentType: '{{ sale.payment_type }}',
|
paymentType: '{{ sale.payment_type|escapejs }}',
|
||||||
paymentMethodId: '{{ payment_method_id }}',
|
paymentMethodId: '{{ payment_method_id|default:"" }}',
|
||||||
paidAmount: {{ sale.paid_amount|default:0 }},
|
paidAmount: {{ sale.paid_amount|default:0 }},
|
||||||
discount: {{ sale.discount|default:0 }},
|
discount: {{ sale.discount|default:0 }},
|
||||||
dueDate: '{{ sale.due_date|date:"Y-m-d" }}',
|
dueDate: '{{ sale.due_date|date:"Y-m-d" }}',
|
||||||
notes: `{{ sale.notes|escapejs }}`,
|
notes: `{{ sale.notes|escapejs }}`,
|
||||||
currencySymbol: '{{ site_settings.currency_symbol }}',
|
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
|
||||||
|
decimalPlaces: {{ decimal_places|default:3 }},
|
||||||
isProcessing: false
|
isProcessing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -217,7 +219,7 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
calculateTotal() {
|
calculateTotal() {
|
||||||
// Computed properties handle this, but we can trigger other logic here if needed
|
// Computed properties handle this
|
||||||
},
|
},
|
||||||
filterProducts() {
|
filterProducts() {
|
||||||
if (this.searchQuery.length > 1) {
|
if (this.searchQuery.length > 1) {
|
||||||
@ -251,6 +253,7 @@
|
|||||||
this.cart.splice(index, 1);
|
this.cart.splice(index, 1);
|
||||||
},
|
},
|
||||||
saveSale() {
|
saveSale() {
|
||||||
|
if (this.isProcessing) return;
|
||||||
this.isProcessing = true; if (!this.customerId && this.paymentType !== 'cash') { alert('{% trans "Credit or Partial payments are not allowed for Guest customers." %}'); this.isProcessing = false; return; }
|
this.isProcessing = true; if (!this.customerId && this.paymentType !== 'cash') { alert('{% trans "Credit or Partial payments are not allowed for Guest customers." %}'); this.isProcessing = false; return; }
|
||||||
|
|
||||||
let actualPaidAmount = 0;
|
let actualPaidAmount = 0;
|
||||||
@ -282,6 +285,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
@ -303,4 +307,5 @@
|
|||||||
}
|
}
|
||||||
}).mount('#saleApp');
|
}).mount('#saleApp');
|
||||||
</script>
|
</script>
|
||||||
|
{% endlocalize %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n static %}
|
{% load i18n static l10n %}
|
||||||
|
|
||||||
{% block title %}{% trans "POS" %} | {{ site_settings.business_name }}{% endblock %}
|
{% block title %}{% trans "POS" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
@ -146,7 +146,7 @@
|
|||||||
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-2" id="productGrid">
|
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-2" id="productGrid">
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
<div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}">
|
<div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}">
|
||||||
<div class="card h-100 shadow-sm product-card p-1" onclick="addToCart({{ product.id }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price }})">
|
<div class="card h-100 shadow-sm product-card p-1" onclick="addToCart({{ product.id|unlocalize }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price|unlocalize }})">
|
||||||
{% if product.image %}
|
{% if product.image %}
|
||||||
<img src="{{ product.image.url }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 80px; object-fit: cover;">
|
<img src="{{ product.image.url }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 80px; object-fit: cover;">
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -351,7 +351,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<button class="btn btn-outline-primary payment-method-btn w-100 py-2 py-md-3 {% if forloop.first %}active{% endif %}"
|
<button class="btn btn-outline-primary payment-method-btn w-100 py-2 py-md-3 {% if forloop.first %}active{% endif %}"
|
||||||
data-id="{{ method.id }}" data-name-en="{{ method.name_en|lower }}"
|
data-id="{{ method.id }}" data-name-en="{{ method.name_en|lower }}"
|
||||||
onclick="selectPaymentMethod(this, '{{ method.id }}')">
|
onclick="selectPaymentMethod(this, '{{ method.id|unlocalize }}')">
|
||||||
<span class="d-md-block">{{ method.name_ar }}</span>
|
<span class="d-md-block">{{ method.name_ar }}</span>
|
||||||
<small class="fw-normal d-none d-md-block">{{ method.name_en }}</small>
|
<small class="fw-normal d-none d-md-block">{{ method.name_en }}</small>
|
||||||
</button>
|
</button>
|
||||||
@ -493,13 +493,14 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
{% localize off %}
|
||||||
<script>
|
<script>
|
||||||
let cart = [];
|
let cart = [];
|
||||||
let lastSaleData = null;
|
let lastSaleData = null;
|
||||||
let selectedPaymentMethodId = null;
|
let selectedPaymentMethodId = null;
|
||||||
let customerLoyalty = null;
|
let customerLoyalty = null;
|
||||||
const lang = '{{ LANGUAGE_CODE }}';
|
const lang = '{{ LANGUAGE_CODE|escapejs }}';
|
||||||
const currency = '{{ site_settings.currency_symbol }}';
|
const currency = '{{ site_settings.currency_symbol|escapejs }}';
|
||||||
const decimalPlaces = {{ site_settings.decimal_places|default:3 }};
|
const decimalPlaces = {{ site_settings.decimal_places|default:3 }};
|
||||||
const loyaltyEnabled = {{ settings.loyalty_enabled|yesno:"true,false" }};
|
const loyaltyEnabled = {{ settings.loyalty_enabled|yesno:"true,false" }};
|
||||||
|
|
||||||
@ -743,6 +744,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processPayment() {
|
function processPayment() {
|
||||||
|
if (document.getElementById("confirmPaymentBtn").disabled && document.getElementById("confirmPaymentBtn").innerText === "{% trans "Processing..." %}") return;
|
||||||
const confirmBtn = document.getElementById('confirmPaymentBtn');
|
const confirmBtn = document.getElementById('confirmPaymentBtn');
|
||||||
const originalText = confirmBtn.innerText;
|
const originalText = confirmBtn.innerText;
|
||||||
confirmBtn.disabled = true;
|
confirmBtn.disabled = true;
|
||||||
@ -1070,4 +1072,5 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{% endlocalize %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n l10n %}
|
||||||
|
|
||||||
{% block title %}{% trans "New Purchase" %} | {{ site_settings.business_name }}{% endblock %}
|
{% block title %}{% trans "New Purchase" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
|
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
||||||
</td>
|
</td>
|
||||||
@ -102,14 +102,14 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">{% trans "Subtotal" %}</span>
|
<span class="text-muted">{% trans "Subtotal" %}</span>
|
||||||
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</span>
|
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
<div class="d-flex justify-content-between mb-4">
|
||||||
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
||||||
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</h4>
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Details -->
|
<!-- Payment Details -->
|
||||||
@ -161,6 +161,7 @@
|
|||||||
|
|
||||||
<!-- Vue.js 3 -->
|
<!-- Vue.js 3 -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
{% localize off %}
|
||||||
<script>
|
<script>
|
||||||
const { createApp } = Vue;
|
const { createApp } = Vue;
|
||||||
|
|
||||||
@ -171,12 +172,12 @@
|
|||||||
products: [
|
products: [
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
{
|
{
|
||||||
id: {{ p.id }},
|
id: {{ p.id|default:0 }},
|
||||||
name_en: "{{ p.name_en }}",
|
name_en: "{{ p.name_en|escapejs }}",
|
||||||
name_ar: "{{ p.name_ar }}",
|
name_ar: "{{ p.name_ar|escapejs }}",
|
||||||
sku: "{{ p.sku }}",
|
sku: "{{ p.sku|escapejs }}",
|
||||||
cost_price: {{ p.cost_price }},
|
cost_price: {{ p.cost_price|default:0 }},
|
||||||
stock: {{ p.stock_quantity }}
|
stock: {{ p.stock_quantity|default:0 }}
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
],
|
],
|
||||||
@ -190,7 +191,8 @@
|
|||||||
paidAmount: 0,
|
paidAmount: 0,
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
currencySymbol: '{{ site_settings.currency_symbol }}',
|
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
|
||||||
|
decimalPlaces: {{ decimal_places|default:3 }},
|
||||||
isProcessing: false
|
isProcessing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -232,6 +234,7 @@
|
|||||||
this.cart.splice(index, 1);
|
this.cart.splice(index, 1);
|
||||||
},
|
},
|
||||||
savePurchase() {
|
savePurchase() {
|
||||||
|
if (this.isProcessing) return;
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
|
|
||||||
let actualPaidAmount = 0;
|
let actualPaidAmount = 0;
|
||||||
@ -262,6 +265,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
@ -283,4 +287,5 @@
|
|||||||
}
|
}
|
||||||
}).mount('#purchaseApp');
|
}).mount('#purchaseApp');
|
||||||
</script>
|
</script>
|
||||||
|
{% endlocalize %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n l10n %}
|
||||||
|
|
||||||
{% block title %}{% trans "New Purchase Return" %} | {{ site_settings.business_name }}{% endblock %}
|
{% block title %}{% trans "New Purchase Return" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
@ -86,7 +86,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
|
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
||||||
</td>
|
</td>
|
||||||
@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">{% trans "Total Amount" %}</span>
|
<span class="text-muted">{% trans "Total Amount" %}</span>
|
||||||
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</h4>
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
@ -141,6 +141,7 @@
|
|||||||
|
|
||||||
<!-- Vue.js 3 -->
|
<!-- Vue.js 3 -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
{% localize off %}
|
||||||
<script>
|
<script>
|
||||||
const { createApp } = Vue;
|
const { createApp } = Vue;
|
||||||
|
|
||||||
@ -151,12 +152,12 @@
|
|||||||
products: [
|
products: [
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
{
|
{
|
||||||
id: {{ p.id }},
|
id: {{ p.id|default:0 }},
|
||||||
name_en: "{{ p.name_en }}",
|
name_en: "{{ p.name_en|escapejs }}",
|
||||||
name_ar: "{{ p.name_ar }}",
|
name_ar: "{{ p.name_ar|escapejs }}",
|
||||||
sku: "{{ p.sku }}",
|
sku: "{{ p.sku|escapejs }}",
|
||||||
cost_price: {{ p.cost_price }},
|
cost_price: {{ p.cost_price|default:0 }},
|
||||||
stock: {{ p.stock_quantity }}
|
stock: {{ p.stock_quantity|default:0 }}
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
],
|
],
|
||||||
@ -167,7 +168,8 @@
|
|||||||
purchaseId: '',
|
purchaseId: '',
|
||||||
returnNumber: '',
|
returnNumber: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
currencySymbol: '{{ site_settings.currency_symbol }}',
|
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
|
||||||
|
decimalPlaces: {{ decimal_places|default:3 }},
|
||||||
isProcessing: false
|
isProcessing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -209,6 +211,7 @@
|
|||||||
this.cart.splice(index, 1);
|
this.cart.splice(index, 1);
|
||||||
},
|
},
|
||||||
saveReturn() {
|
saveReturn() {
|
||||||
|
if (this.isProcessing) return;
|
||||||
if (!confirm("{% trans 'Are you sure you want to process this purchase return? This will deduct from stock.' %}")) return;
|
if (!confirm("{% trans 'Are you sure you want to process this purchase return? This will deduct from stock.' %}")) return;
|
||||||
|
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
@ -231,6 +234,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
@ -252,4 +256,5 @@
|
|||||||
}
|
}
|
||||||
}).mount('#returnApp');
|
}).mount('#returnApp');
|
||||||
</script>
|
</script>
|
||||||
|
{% endlocalize %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n l10n %}
|
||||||
|
|
||||||
{% block title %}{% trans "New Quotation" %} | {{ site_settings.business_name }}{% endblock %}
|
{% block title %}{% trans "New Quotation" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
|
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
||||||
</td>
|
</td>
|
||||||
@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">{% trans "Subtotal" %}</span>
|
<span class="text-muted">{% trans "Subtotal" %}</span>
|
||||||
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</span>
|
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
@ -125,7 +125,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
<div class="d-flex justify-content-between mb-4">
|
||||||
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
||||||
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(3) ]]</h4>
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(decimalPlaces) ]]</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@ -153,6 +153,7 @@
|
|||||||
|
|
||||||
<!-- Vue.js 3 -->
|
<!-- Vue.js 3 -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
{% localize off %}
|
||||||
<script>
|
<script>
|
||||||
const { createApp } = Vue;
|
const { createApp } = Vue;
|
||||||
|
|
||||||
@ -163,12 +164,12 @@
|
|||||||
products: [
|
products: [
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
{
|
{
|
||||||
id: {{ p.id }},
|
id: {{ p.id|default:0 }},
|
||||||
name_en: "{{ p.name_en }}",
|
name_en: "{{ p.name_en|escapejs }}",
|
||||||
name_ar: "{{ p.name_ar }}",
|
name_ar: "{{ p.name_ar|escapejs }}",
|
||||||
sku: "{{ p.sku }}",
|
sku: "{{ p.sku|escapejs }}",
|
||||||
price: {{ p.price }},
|
price: {{ p.price|default:0 }},
|
||||||
stock: {{ p.stock_quantity }}
|
stock: {{ p.stock_quantity|default:0 }}
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
],
|
],
|
||||||
@ -181,7 +182,8 @@
|
|||||||
validUntil: '',
|
validUntil: '',
|
||||||
termsAndConditions: '1. Prices are valid for 7 days.\n2. Delivery within 3-5 working days.\n3. Payment: 50% advance, 50% on delivery.',
|
termsAndConditions: '1. Prices are valid for 7 days.\n2. Delivery within 3-5 working days.\n3. Payment: 50% advance, 50% on delivery.',
|
||||||
notes: '',
|
notes: '',
|
||||||
currencySymbol: '{{ site_settings.currency_symbol }}',
|
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
|
||||||
|
decimalPlaces: {{ decimal_places|default:3 }},
|
||||||
isProcessing: false
|
isProcessing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -226,6 +228,7 @@
|
|||||||
this.cart.splice(index, 1);
|
this.cart.splice(index, 1);
|
||||||
},
|
},
|
||||||
saveQuotation() {
|
saveQuotation() {
|
||||||
|
if (this.isProcessing) return;
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -248,6 +251,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
@ -269,4 +273,5 @@
|
|||||||
}
|
}
|
||||||
}).mount('#quotationApp');
|
}).mount('#quotationApp');
|
||||||
</script>
|
</script>
|
||||||
|
{% endlocalize %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n l10n %}
|
||||||
|
|
||||||
{% block title %}{% trans "New Sales Return" %} | {{ site_settings.business_name }}{% endblock %}
|
{% block title %}{% trans "New Sales Return" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
@ -86,7 +86,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
|
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
||||||
</td>
|
</td>
|
||||||
@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">{% trans "Total Amount" %}</span>
|
<span class="text-muted">{% trans "Total Amount" %}</span>
|
||||||
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</h4>
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
@ -141,6 +141,7 @@
|
|||||||
|
|
||||||
<!-- Vue.js 3 -->
|
<!-- Vue.js 3 -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
{% localize off %}
|
||||||
<script>
|
<script>
|
||||||
const { createApp } = Vue;
|
const { createApp } = Vue;
|
||||||
|
|
||||||
@ -151,12 +152,12 @@
|
|||||||
products: [
|
products: [
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
{
|
{
|
||||||
id: {{ p.id }},
|
id: {{ p.id|default:0 }},
|
||||||
name_en: "{{ p.name_en }}",
|
name_en: "{{ p.name_en|escapejs }}",
|
||||||
name_ar: "{{ p.name_ar }}",
|
name_ar: "{{ p.name_ar|escapejs }}",
|
||||||
sku: "{{ p.sku }}",
|
sku: "{{ p.sku|escapejs }}",
|
||||||
price: {{ p.price }},
|
price: {{ p.price|default:0 }},
|
||||||
stock: {{ p.stock_quantity }}
|
stock: {{ p.stock_quantity|default:0 }}
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
],
|
],
|
||||||
@ -167,7 +168,8 @@
|
|||||||
saleId: '',
|
saleId: '',
|
||||||
returnNumber: '',
|
returnNumber: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
currencySymbol: '{{ site_settings.currency_symbol }}',
|
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
|
||||||
|
decimalPlaces: {{ decimal_places|default:3 }},
|
||||||
isProcessing: false
|
isProcessing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -209,6 +211,7 @@
|
|||||||
this.cart.splice(index, 1);
|
this.cart.splice(index, 1);
|
||||||
},
|
},
|
||||||
saveReturn() {
|
saveReturn() {
|
||||||
|
if (this.isProcessing) return;
|
||||||
if (!confirm("{% trans 'Are you sure you want to process this return? This will update stock.' %}")) return;
|
if (!confirm("{% trans 'Are you sure you want to process this return? This will update stock.' %}")) return;
|
||||||
|
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
@ -231,6 +234,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
@ -252,4 +256,5 @@
|
|||||||
}
|
}
|
||||||
}).mount('#returnApp');
|
}).mount('#returnApp');
|
||||||
</script>
|
</script>
|
||||||
|
{% endlocalize %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -40,6 +40,11 @@
|
|||||||
<i class="bi bi-star me-2"></i>{% trans "Loyalty System" %}
|
<i class="bi bi-star me-2"></i>{% trans "Loyalty System" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link fw-bold px-4" id="whatsapp-tab" data-bs-toggle="pill" data-bs-target="#whatsapp" type="button" role="tab">
|
||||||
|
<i class="bi bi-whatsapp me-2"></i>{% trans "WhatsApp Gateway" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content" id="settingsTabsContent">
|
<div class="tab-content" id="settingsTabsContent">
|
||||||
@ -389,6 +394,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- WhatsApp Tab -->
|
||||||
|
<div class="tab-pane fade" id="whatsapp" role="tabpanel">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm border-0 glassmorphism mb-4">
|
||||||
|
<div class="card-header bg-transparent border-0 py-3">
|
||||||
|
<h5 class="card-title mb-0 fw-bold">{% trans "Wablas WhatsApp Integration" %}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{% url 'settings' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="wablas_enabled" {% if settings.wablas_enabled %}checked{% endif %}>
|
||||||
|
<label class="form-check-label fw-bold">{% trans "Enable WhatsApp Gateway" %}</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small">{% trans "When enabled, you can send automated messages, invoices, and alerts via WhatsApp using the Wablas gateway." %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label fw-semibold">{% trans "Wablas API Token" %}</label>
|
||||||
|
<input type="text" name="wablas_token" class="form-control" value="{{ settings.wablas_token }}" placeholder="e.g. your_api_token_here">
|
||||||
|
<div class="form-text">{% trans "Get your token from your Wablas dashboard." %}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label fw-semibold">{% trans "Wablas Server URL" %}</label>
|
||||||
|
<input type="url" name="wablas_server_url" class="form-control" value="{{ settings.wablas_server_url }}" placeholder="https://console.wablas.com">
|
||||||
|
<div class="form-text">{% trans "Ensure it starts with https://. Example: https://console.wablas.com or your custom domain." %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-3 border-top d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-success px-4 py-2">
|
||||||
|
<i class="bi bi-whatsapp me-2"></i> {% trans "Save WhatsApp Settings" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-0 glassmorphism bg-light border-start border-4 border-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="fw-bold text-success"><i class="bi bi-info-circle me-2"></i>{% trans "How to use" %}</h6>
|
||||||
|
<ul class="mb-0 small text-muted">
|
||||||
|
<li>{% trans "Register an account at Wablas.com" %}</li>
|
||||||
|
<li>{% trans "Scan your WhatsApp QR code in the Wablas dashboard." %}</li>
|
||||||
|
<li>{% trans "Copy the API Token and Server URL into the fields above." %}</li>
|
||||||
|
<li>{% trans "Test by sending a sample message to your own number." %}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card shadow-sm border-0 glassmorphism mb-4">
|
||||||
|
<div class="card-header bg-transparent border-0 py-3">
|
||||||
|
<h5 class="card-title mb-0 fw-bold">{% trans "Test Connection" %}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted small">{% trans "Verify your WhatsApp gateway is working correctly." %}</p>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Test Phone Number" %}</label>
|
||||||
|
<input type="text" id="test_phone" class="form-control form-control-sm" placeholder="e.g. 96891234567">
|
||||||
|
</div>
|
||||||
|
<button type="button" id="btn_test_whatsapp" class="btn btn-outline-success w-100 btn-sm">
|
||||||
|
<i class="bi bi-send me-1"></i> {% trans "Send Test Message" %}
|
||||||
|
</button>
|
||||||
|
<div id="test_result" class="mt-3 d-none"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -508,6 +589,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name_en: nameEn,
|
name_en: nameEn,
|
||||||
@ -556,8 +638,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('savePm').addEventListener('click', () => savePaymentMethod(false));
|
const savePmBtn = document.getElementById('savePm');
|
||||||
document.getElementById('saveAndAddAnotherPm').addEventListener('click', () => savePaymentMethod(true));
|
if (savePmBtn) savePmBtn.addEventListener('click', () => savePaymentMethod(false));
|
||||||
|
|
||||||
|
const saveAndAddAnotherPmBtn = document.getElementById('saveAndAddAnotherPm');
|
||||||
|
if (saveAndAddAnotherPmBtn) saveAndAddAnotherPmBtn.addEventListener('click', () => savePaymentMethod(true));
|
||||||
|
|
||||||
|
// WhatsApp Test
|
||||||
|
const btnTestWhatsapp = document.getElementById('btn_test_whatsapp');
|
||||||
|
if (btnTestWhatsapp) {
|
||||||
|
btnTestWhatsapp.addEventListener('click', async function() {
|
||||||
|
const phone = document.getElementById('test_phone').value;
|
||||||
|
const resultDiv = document.getElementById('test_result');
|
||||||
|
|
||||||
|
if (!phone) {
|
||||||
|
alert('{% trans "Please enter a test phone number" %}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnTestWhatsapp.disabled = true;
|
||||||
|
btnTestWhatsapp.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> {% trans "Sending..." %}';
|
||||||
|
resultDiv.classList.add('d-none');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('{% url "test_whatsapp_connection" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ phone: phone })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
resultDiv.classList.remove('d-none');
|
||||||
|
if (data.success) {
|
||||||
|
resultDiv.className = 'mt-3 alert alert-success small py-2';
|
||||||
|
resultDiv.innerHTML = '<i class="bi bi-check-circle me-1"></i>' + data.message;
|
||||||
|
} else {
|
||||||
|
resultDiv.className = 'mt-3 alert alert-danger small py-2';
|
||||||
|
resultDiv.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>' + data.error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
resultDiv.classList.remove('d-none');
|
||||||
|
resultDiv.className = 'mt-3 alert alert-danger small py-2';
|
||||||
|
resultDiv.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i> {% trans "An error occurred." %}';
|
||||||
|
} finally {
|
||||||
|
btnTestWhatsapp.disabled = false;
|
||||||
|
btnTestWhatsapp.innerHTML = '<i class="bi bi-send me-1"></i> {% trans "Send Test Message" %}';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -3,6 +3,42 @@
|
|||||||
|
|
||||||
{% block title %}{% trans "User & Role Management" %} - {{ site_settings.business_name }}{% endblock %}
|
{% block title %}{% trans "User & Role Management" %} - {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
.permissions-container {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #dee2e6 #f8f9fa;
|
||||||
|
}
|
||||||
|
.permissions-container::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.permissions-container::-webkit-scrollbar-track {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.permissions-container::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #dee2e6;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.permissions-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,.1);
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.permissions-table tbody tr:hover {
|
||||||
|
background-color: rgba(0, 123, 255, 0.02);
|
||||||
|
}
|
||||||
|
.form-check-input {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2 class="h4 mb-0 fw-bold">{% trans "User & Role Management" %}</h2>
|
<h2 class="h4 mb-0 fw-bold">{% trans "User & Role Management" %}</h2>
|
||||||
@ -138,7 +174,7 @@
|
|||||||
data-id="{{ g.id }}"
|
data-id="{{ g.id }}"
|
||||||
data-name="{{ g.name }}"
|
data-name="{{ g.name }}"
|
||||||
data-bs-toggle="modal" data-bs-target="#editGroupModal">
|
data-bs-toggle="modal" data-bs-target="#editGroupModal">
|
||||||
<i class="bi bi-shield-lock"></i> Edit Perms
|
<i class="bi bi-shield-lock me-1"></i> {% trans "Edit Permissions" %}
|
||||||
</button>
|
</button>
|
||||||
<form action="{% url 'user_management' %}" method="post" class="d-inline" onsubmit="return confirm('{% trans "Are you sure you want to delete this group?" %}');">
|
<form action="{% url 'user_management' %}" method="post" class="d-inline" onsubmit="return confirm('{% trans "Are you sure you want to delete this group?" %}');">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@ -260,17 +296,67 @@
|
|||||||
<input type="text" name="name" class="form-control rounded-3" required placeholder="e.g., Inventory Manager">
|
<input type="text" name="name" class="form-control rounded-3" required placeholder="e.g., Inventory Manager">
|
||||||
</div>
|
</div>
|
||||||
<h6 class="fw-bold mb-3 small text-uppercase text-muted">{% trans "Assign Permissions" %}</h6>
|
<h6 class="fw-bold mb-3 small text-uppercase text-muted">{% trans "Assign Permissions" %}</h6>
|
||||||
<div class="row g-3" style="max-height: 400px; overflow-y: auto;">
|
<div class="permissions-container px-2" style="max-height: 500px; overflow-y: auto;">
|
||||||
{% for perm in permissions %}
|
<div class="table-responsive">
|
||||||
<div class="col-md-6 col-lg-4">
|
<table class="table table-hover align-middle mb-0 permissions-table">
|
||||||
<div class="form-check card p-2 border-light shadow-none">
|
<thead class="bg-light">
|
||||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="permissions" value="{{ perm.id }}" id="perm_{{ perm.id }}">
|
<tr class="small text-uppercase fw-bold text-muted">
|
||||||
<label class="form-check-label small d-block" for="perm_{{ perm.id }}">
|
<th class="ps-3" style="width: 40%;">{% trans "Module" %}</th>
|
||||||
<span class="text-primary fw-bold">{{ perm.content_type.app_label }}</span> | {{ perm.name }}
|
<th class="text-center">{% trans "View" %}</th>
|
||||||
</label>
|
<th class="text-center">{% trans "Add" %}</th>
|
||||||
</div>
|
<th class="text-center">{% trans "Edit" %}</th>
|
||||||
|
<th class="text-center">{% trans "Delete" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% regroup permissions by content_type as perm_list %}
|
||||||
|
{% for g in perm_list %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-3 py-3">
|
||||||
|
<div class="fw-bold text-dark">{{ g.grouper.name|capfirst }}</div>
|
||||||
|
<div class="text-muted small">{{ g.grouper.app_label }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for perm in g.list %}
|
||||||
|
{% if "view" in perm.codename %}
|
||||||
|
<div class="form-check d-inline-block">
|
||||||
|
<input class="form-check-input" type="checkbox" name="permissions" value="{{ perm.id }}" id="perm_{{ perm.id }}">
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for perm in g.list %}
|
||||||
|
{% if "add" in perm.codename %}
|
||||||
|
<div class="form-check d-inline-block">
|
||||||
|
<input class="form-check-input" type="checkbox" name="permissions" value="{{ perm.id }}" id="perm_{{ perm.id }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for perm in g.list %}
|
||||||
|
{% if "change" in perm.codename %}
|
||||||
|
<div class="form-check d-inline-block">
|
||||||
|
<input class="form-check-input" type="checkbox" name="permissions" value="{{ perm.id }}" id="perm_{{ perm.id }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for perm in g.list %}
|
||||||
|
{% if "delete" in perm.codename %}
|
||||||
|
<div class="form-check d-inline-block">
|
||||||
|
<input class="form-check-input" type="checkbox" name="permissions" value="{{ perm.id }}" id="perm_{{ perm.id }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 pt-0">
|
<div class="modal-footer border-0 pt-0">
|
||||||
@ -300,17 +386,67 @@
|
|||||||
<input type="text" name="name" id="editGroupName" class="form-control rounded-3" required>
|
<input type="text" name="name" id="editGroupName" class="form-control rounded-3" required>
|
||||||
</div>
|
</div>
|
||||||
<h6 class="fw-bold mb-3 small text-uppercase text-muted">{% trans "Permissions" %}</h6>
|
<h6 class="fw-bold mb-3 small text-uppercase text-muted">{% trans "Permissions" %}</h6>
|
||||||
<div class="row g-3" style="max-height: 400px; overflow-y: auto;">
|
<div class="permissions-container px-2" style="max-height: 500px; overflow-y: auto;">
|
||||||
{% for perm in permissions %}
|
<div class="table-responsive">
|
||||||
<div class="col-md-6 col-lg-4">
|
<table class="table table-hover align-middle mb-0 permissions-table">
|
||||||
<div class="form-check card p-2 border-light shadow-none">
|
<thead class="bg-light">
|
||||||
<input class="form-check-input edit-group-perm ms-0 me-2" type="checkbox" name="permissions" value="{{ perm.id }}" id="edit_perm_{{ perm.id }}">
|
<tr class="small text-uppercase fw-bold text-muted">
|
||||||
<label class="form-check-label small d-block" for="edit_perm_{{ perm.id }}">
|
<th class="ps-3" style="width: 40%;">{% trans "Module" %}</th>
|
||||||
<span class="text-primary fw-bold">{{ perm.content_type.app_label }}</span> | {{ perm.name }}
|
<th class="text-center">{% trans "View" %}</th>
|
||||||
</label>
|
<th class="text-center">{% trans "Add" %}</th>
|
||||||
</div>
|
<th class="text-center">{% trans "Edit" %}</th>
|
||||||
|
<th class="text-center">{% trans "Delete" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% regroup permissions by content_type as perm_list %}
|
||||||
|
{% for g in perm_list %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-3 py-3">
|
||||||
|
<div class="fw-bold text-dark">{{ g.grouper.name|capfirst }}</div>
|
||||||
|
<div class="text-muted small">{{ g.grouper.app_label }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for perm in g.list %}
|
||||||
|
{% if "view" in perm.codename %}
|
||||||
|
<div class="form-check d-inline-block">
|
||||||
|
<input class="form-check-input edit-group-perm" type="checkbox" name="permissions" value="{{ perm.id }}" id="edit_perm_{{ perm.id }}">
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for perm in g.list %}
|
||||||
|
{% if "add" in perm.codename %}
|
||||||
|
<div class="form-check d-inline-block">
|
||||||
|
<input class="form-check-input edit-group-perm" type="checkbox" name="permissions" value="{{ perm.id }}" id="edit_perm_{{ perm.id }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for perm in g.list %}
|
||||||
|
{% if "change" in perm.codename %}
|
||||||
|
<div class="form-check d-inline-block">
|
||||||
|
<input class="form-check-input edit-group-perm" type="checkbox" name="permissions" value="{{ perm.id }}" id="edit_perm_{{ perm.id }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for perm in g.list %}
|
||||||
|
{% if "delete" in perm.codename %}
|
||||||
|
<div class="form-check d-inline-block">
|
||||||
|
<input class="form-check-input edit-group-perm" type="checkbox" name="permissions" value="{{ perm.id }}" id="edit_perm_{{ perm.id }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 pt-0">
|
<div class="modal-footer border-0 pt-0">
|
||||||
|
|||||||
@ -118,4 +118,6 @@ urlpatterns = [
|
|||||||
path('settings/loyalty/edit/<int:pk>/', views.edit_loyalty_tier, name='edit_loyalty_tier'),
|
path('settings/loyalty/edit/<int:pk>/', views.edit_loyalty_tier, name='edit_loyalty_tier'),
|
||||||
path('settings/loyalty/delete/<int:pk>/', views.delete_loyalty_tier, name='delete_loyalty_tier'),
|
path('settings/loyalty/delete/<int:pk>/', views.delete_loyalty_tier, name='delete_loyalty_tier'),
|
||||||
path('api/customer-loyalty/<int:pk>/', views.get_customer_loyalty_api, name='get_customer_loyalty_api'),
|
path('api/customer-loyalty/<int:pk>/', views.get_customer_loyalty_api, name='get_customer_loyalty_api'),
|
||||||
|
# WhatsApp
|
||||||
|
path('api/test-whatsapp/', views.test_whatsapp_connection, name='test_whatsapp_connection'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -62,3 +62,44 @@ def number_to_words_ar(number):
|
|||||||
# if I can, or just use English for both if not specified.
|
# if I can, or just use English for both if not specified.
|
||||||
# However, I'll try to provide a basic one if possible.
|
# However, I'll try to provide a basic one if possible.
|
||||||
return number_to_words_en(number) # Fallback to EN for now to ensure it works.
|
return number_to_words_en(number) # Fallback to EN for now to ensure it works.
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def send_whatsapp_message(phone, message):
|
||||||
|
"""
|
||||||
|
Sends a WhatsApp message via Wablas gateway.
|
||||||
|
"""
|
||||||
|
from .models import SystemSetting
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
|
||||||
|
if not settings or not settings.wablas_enabled:
|
||||||
|
return False, "WhatsApp gateway is disabled."
|
||||||
|
|
||||||
|
if not settings.wablas_token or not settings.wablas_server_url:
|
||||||
|
return False, "Wablas configuration is incomplete."
|
||||||
|
|
||||||
|
# Clean phone number (remove non-digits)
|
||||||
|
phone = ''.join(filter(str.isdigit, str(phone)))
|
||||||
|
|
||||||
|
# Ensure URL is properly formatted
|
||||||
|
server_url = settings.wablas_server_url.rstrip('/')
|
||||||
|
url = f"{server_url}/api/send-message"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": settings.wablas_token
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"phone": phone,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, data=payload, headers=headers, timeout=10)
|
||||||
|
data = response.json()
|
||||||
|
if response.status_code == 200 and data.get('status') == True:
|
||||||
|
return True, "Message sent successfully."
|
||||||
|
else:
|
||||||
|
return False, data.get('message', 'Unknown error from Wablas.')
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
@ -914,6 +914,12 @@ def settings_view(request):
|
|||||||
settings.currency_per_point = request.POST.get('currency_per_point', 0.010)
|
settings.currency_per_point = request.POST.get('currency_per_point', 0.010)
|
||||||
settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100)
|
settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100)
|
||||||
|
|
||||||
|
# WhatsApp Settings
|
||||||
|
if "wablas_token" in request.POST:
|
||||||
|
settings.wablas_enabled = request.POST.get("wablas_enabled") == "on"
|
||||||
|
settings.wablas_token = request.POST.get("wablas_token", "")
|
||||||
|
settings.wablas_server_url = request.POST.get("wablas_server_url", "")
|
||||||
|
|
||||||
if 'logo' in request.FILES:
|
if 'logo' in request.FILES:
|
||||||
settings.logo = request.FILES['logo']
|
settings.logo = request.FILES['logo']
|
||||||
|
|
||||||
@ -1336,7 +1342,8 @@ def user_management(request):
|
|||||||
users = paginator.get_page(page_number)
|
users = paginator.get_page(page_number)
|
||||||
groups = Group.objects.all().prefetch_related('permissions')
|
groups = Group.objects.all().prefetch_related('permissions')
|
||||||
# Filter for relevant permissions (core and auth)
|
# Filter for relevant permissions (core and auth)
|
||||||
permissions = Permission.objects.select_related('content_type').all().order_by('content_type__app_label', 'codename')
|
excluded_apps = ['admin', 'auth', 'contenttypes', 'sessions']
|
||||||
|
permissions = Permission.objects.select_related('content_type').exclude(content_type__app_label__in=excluded_apps).order_by('content_type__app_label', 'content_type__model', 'codename')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
action = request.POST.get('action')
|
action = request.POST.get('action')
|
||||||
@ -2215,3 +2222,24 @@ def cashflow_report(request):
|
|||||||
'settings': settings
|
'settings': settings
|
||||||
}
|
}
|
||||||
return render(request, 'core/cashflow_report.html', context)
|
return render(request, 'core/cashflow_report.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def test_whatsapp_connection(request):
|
||||||
|
"""
|
||||||
|
AJAX view to test the WhatsApp connection.
|
||||||
|
"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
phone = data.get('phone')
|
||||||
|
if not phone:
|
||||||
|
return JsonResponse({'success': False, 'error': _("Phone number is required.")})
|
||||||
|
|
||||||
|
from .utils import send_whatsapp_message
|
||||||
|
success, message = send_whatsapp_message(phone, _("Hello! This is a test message from your Meezan Smart Admin WhatsApp Gateway."))
|
||||||
|
|
||||||
|
return JsonResponse({'success': success, 'message': message})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)})
|
||||||
|
return JsonResponse({'success': False, 'error': _("Invalid request method.")})
|
||||||
|
|||||||
Binary file not shown.
@ -3,7 +3,7 @@
|
|||||||
# This file is distributed under the same license as the PACKAGE package.
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
@ -3092,7 +3092,7 @@ msgid "Group name already exists."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/views.py:1401
|
#: core/views.py:1401
|
||||||
#, fuzzy
|
#
|
||||||
#| msgid "Completed"
|
#| msgid "Completed"
|
||||||
msgid "Group deleted."
|
msgid "Group deleted."
|
||||||
msgstr "مكتمل"
|
msgstr "مكتمل"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user