diff --git a/accounting/__pycache__/signals.cpython-311.pyc b/accounting/__pycache__/signals.cpython-311.pyc index bd9edfe..7f8a8b7 100644 Binary files a/accounting/__pycache__/signals.cpython-311.pyc and b/accounting/__pycache__/signals.cpython-311.pyc differ diff --git a/accounting/signals.py b/accounting/signals.py index 20a792e..6d0060c 100644 --- a/accounting/signals.py +++ b/accounting/signals.py @@ -1,3 +1,4 @@ +from django.utils import timezone from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from core.models import Sale, SalePayment, Purchase, PurchasePayment, Expense, SaleReturn, PurchaseReturn diff --git a/assets/pasted-20260203-040015-70bc78c5.jpg b/assets/pasted-20260203-040015-70bc78c5.jpg new file mode 100644 index 0000000..56c9cdf Binary files /dev/null and b/assets/pasted-20260203-040015-70bc78c5.jpg differ diff --git a/assets/vm-shot-2026-02-03T03-59-49-172Z.jpg b/assets/vm-shot-2026-02-03T03-59-49-172Z.jpg new file mode 100644 index 0000000..56c9cdf Binary files /dev/null and b/assets/vm-shot-2026-02-03T03-59-49-172Z.jpg differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 80bb7d0..4c793a1 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 0a5b204..560a7a0 100644 --- a/config/settings.py +++ b/config/settings.py @@ -26,6 +26,8 @@ ALLOWED_HOSTS = [ os.getenv("HOST_FQDN", ""), ] +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + CSRF_TRUSTED_ORIGINS = [ origin for origin in [ os.getenv("HOST_FQDN", ""), @@ -37,11 +39,13 @@ 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 CSRF_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = "None" CSRF_COOKIE_SAMESITE = "None" +LANGUAGE_COOKIE_SECURE = True +LANGUAGE_COOKIE_SAMESITE = "None" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ @@ -85,6 +89,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', '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 'core.context_processors.project_context', 'core.context_processors.global_settings', @@ -196,4 +201,4 @@ LOGIN_URL = '/accounts/login/' # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 48736bb..5a0a5f0 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 21142d0..1e7cf90 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/utils.cpython-311.pyc b/core/__pycache__/utils.cpython-311.pyc index f1acd99..fcfe627 100644 Binary files a/core/__pycache__/utils.cpython-311.pyc and b/core/__pycache__/utils.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 3347185..b5bcced 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0018_systemsetting_wablas_enabled_and_more.py b/core/migrations/0018_systemsetting_wablas_enabled_and_more.py new file mode 100644 index 0000000..2e3bb1b --- /dev/null +++ b/core/migrations/0018_systemsetting_wablas_enabled_and_more.py @@ -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'), + ), + ] diff --git a/core/migrations/__pycache__/0018_systemsetting_wablas_enabled_and_more.cpython-311.pyc b/core/migrations/__pycache__/0018_systemsetting_wablas_enabled_and_more.cpython-311.pyc new file mode 100644 index 0000000..d5542d8 Binary files /dev/null and b/core/migrations/__pycache__/0018_systemsetting_wablas_enabled_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 157d084..016d2a5 100644 --- a/core/models.py +++ b/core/models.py @@ -360,6 +360,11 @@ class SystemSetting(models.Model): points_per_currency = models.DecimalField(_("Points Earned per Currency Unit"), max_digits=10, decimal_places=2, default=1.0) 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) + + # 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): return self.business_name diff --git a/core/templates/base.html b/core/templates/base.html index 35c7f35..6ea8a9e 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -386,6 +386,27 @@ } }); {% 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 = `${originalText}`; + } + }); + } + }); {% block scripts %}{% endblock %} diff --git a/core/templates/core/invoice_create.html b/core/templates/core/invoice_create.html index dfefabd..56cb8f2 100644 --- a/core/templates/core/invoice_create.html +++ b/core/templates/core/invoice_create.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load i18n %} +{% load i18n l10n %} {% block title %}{% trans "New Invoice" %} | {{ site_settings.business_name }}{% endblock %} @@ -77,7 +77,7 @@ - [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] @@ -102,7 +102,7 @@
{% trans "Subtotal" %} - [[ currencySymbol ]][[ subtotal.toFixed(3) ]] + [[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]
@@ -116,7 +116,7 @@

{% trans "Grand Total" %}

-

[[ currencySymbol ]][[ grandTotal.toFixed(3) ]]

+

[[ currencySymbol ]][[ grandTotal.toFixed(decimalPlaces) ]]

@@ -168,6 +168,7 @@ +{% localize off %} +{% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/templates/core/invoice_edit.html b/core/templates/core/invoice_edit.html index d9f900c..64b76f3 100644 --- a/core/templates/core/invoice_edit.html +++ b/core/templates/core/invoice_edit.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load i18n %} +{% load i18n l10n %} {% block title %}{% trans "Edit Invoice" %} | {{ site_settings.business_name }}{% endblock %} @@ -82,7 +82,7 @@ - [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + [[ currencySymbol ]][[ (parseFloat(item.price) * parseFloat(item.quantity)).toFixed(decimalPlaces) ]] @@ -107,7 +107,7 @@
{% trans "Subtotal" %} - [[ currencySymbol ]][[ subtotal.toFixed(3) ]] + [[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]
@@ -121,7 +121,7 @@

{% trans "Grand Total" %}

-

[[ currencySymbol ]][[ grandTotal.toFixed(3) ]]

+

[[ currencySymbol ]][[ grandTotal.toFixed(decimalPlaces) ]]

@@ -173,6 +173,7 @@ +{% localize off %} +{% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html index be555d4..a017838 100644 --- a/core/templates/core/pos.html +++ b/core/templates/core/pos.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load i18n static %} +{% load i18n static l10n %} {% block title %}{% trans "POS" %} | {{ site_settings.business_name }}{% endblock %} @@ -146,7 +146,7 @@
{% for product in products %}
-
+
{% if product.image %} {{ product.name_en }} {% else %} @@ -351,7 +351,7 @@
@@ -493,13 +493,14 @@ {% endblock %} {% block scripts %} +{% localize off %} +{% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/templates/core/purchase_create.html b/core/templates/core/purchase_create.html index 9892f93..6769ba5 100644 --- a/core/templates/core/purchase_create.html +++ b/core/templates/core/purchase_create.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load i18n %} +{% load i18n l10n %} {% block title %}{% trans "New Purchase" %} | {{ site_settings.business_name }}{% endblock %} @@ -77,7 +77,7 @@ - [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] @@ -102,14 +102,14 @@
{% trans "Subtotal" %} - [[ currencySymbol ]][[ subtotal.toFixed(3) ]] + [[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]

{% trans "Grand Total" %}

-

[[ currencySymbol ]][[ subtotal.toFixed(3) ]]

+

[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]

@@ -161,6 +161,7 @@ +{% localize off %} +{% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/templates/core/purchase_return_create.html b/core/templates/core/purchase_return_create.html index 2dd5559..1cbc84f 100644 --- a/core/templates/core/purchase_return_create.html +++ b/core/templates/core/purchase_return_create.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load i18n %} +{% load i18n l10n %} {% block title %}{% trans "New Purchase Return" %} | {{ site_settings.business_name }}{% endblock %} @@ -86,7 +86,7 @@ - [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] @@ -111,7 +111,7 @@
{% trans "Total Amount" %} -

[[ currencySymbol ]][[ subtotal.toFixed(3) ]]

+

[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]


@@ -141,6 +141,7 @@ +{% localize off %} +{% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/templates/core/quotation_create.html b/core/templates/core/quotation_create.html index 2635ca0..3acd4f0 100644 --- a/core/templates/core/quotation_create.html +++ b/core/templates/core/quotation_create.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load i18n %} +{% load i18n l10n %} {% block title %}{% trans "New Quotation" %} | {{ site_settings.business_name }}{% endblock %} @@ -77,7 +77,7 @@ - [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] @@ -111,7 +111,7 @@
{% trans "Subtotal" %} - [[ currencySymbol ]][[ subtotal.toFixed(3) ]] + [[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]
@@ -125,7 +125,7 @@

{% trans "Grand Total" %}

-

[[ currencySymbol ]][[ grandTotal.toFixed(3) ]]

+

[[ currencySymbol ]][[ grandTotal.toFixed(decimalPlaces) ]]

@@ -153,6 +153,7 @@ +{% localize off %} +{% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/templates/core/sale_return_create.html b/core/templates/core/sale_return_create.html index 132a434..81e9e96 100644 --- a/core/templates/core/sale_return_create.html +++ b/core/templates/core/sale_return_create.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load i18n %} +{% load i18n l10n %} {% block title %}{% trans "New Sales Return" %} | {{ site_settings.business_name }}{% endblock %} @@ -86,7 +86,7 @@ - [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] @@ -111,7 +111,7 @@
{% trans "Total Amount" %} -

[[ currencySymbol ]][[ subtotal.toFixed(3) ]]

+

[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]


@@ -141,6 +141,7 @@ +{% localize off %} +{% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/templates/core/settings.html b/core/templates/core/settings.html index b0817c7..4e38752 100644 --- a/core/templates/core/settings.html +++ b/core/templates/core/settings.html @@ -40,6 +40,11 @@ {% trans "Loyalty System" %} +
@@ -389,6 +394,82 @@
+ + +
+
+
+
+
+
{% trans "Wablas WhatsApp Integration" %}
+
+
+
+ {% csrf_token %} +
+
+
+ + +
+

{% trans "When enabled, you can send automated messages, invoices, and alerts via WhatsApp using the Wablas gateway." %}

+
+ +
+ + +
{% trans "Get your token from your Wablas dashboard." %}
+
+ +
+ + +
{% trans "Ensure it starts with https://. Example: https://console.wablas.com or your custom domain." %}
+
+
+ +
+ +
+
+
+
+ +
+
+
{% trans "How to use" %}
+
    +
  • {% trans "Register an account at Wablas.com" %}
  • +
  • {% trans "Scan your WhatsApp QR code in the Wablas dashboard." %}
  • +
  • {% trans "Copy the API Token and Server URL into the fields above." %}
  • +
  • {% trans "Test by sending a sample message to your own number." %}
  • +
+
+
+
+ +
+
+
+
{% trans "Test Connection" %}
+
+
+

{% trans "Verify your WhatsApp gateway is working correctly." %}

+
+ + +
+ +
+
+
+
+
+
@@ -508,6 +589,7 @@ method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}' }, body: JSON.stringify({ name_en: nameEn, @@ -556,8 +638,58 @@ } } - document.getElementById('savePm').addEventListener('click', () => savePaymentMethod(false)); - document.getElementById('saveAndAddAnotherPm').addEventListener('click', () => savePaymentMethod(true)); + const savePmBtn = document.getElementById('savePm'); + 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 = ' {% 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 = '' + data.message; + } else { + resultDiv.className = 'mt-3 alert alert-danger small py-2'; + resultDiv.innerHTML = '' + 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 = ' {% trans "An error occurred." %}'; + } finally { + btnTestWhatsapp.disabled = false; + btnTestWhatsapp.innerHTML = ' {% trans "Send Test Message" %}'; + } + }); + } }); {% endblock %} \ No newline at end of file diff --git a/core/templates/core/users.html b/core/templates/core/users.html index 9bbcc96..d053fe9 100644 --- a/core/templates/core/users.html +++ b/core/templates/core/users.html @@ -3,6 +3,42 @@ {% block title %}{% trans "User & Role Management" %} - {{ site_settings.business_name }}{% endblock %} +{% block head %} + +{% endblock %} + {% block content %}

{% trans "User & Role Management" %}

@@ -138,7 +174,7 @@ data-id="{{ g.id }}" data-name="{{ g.name }}" data-bs-toggle="modal" data-bs-target="#editGroupModal"> - Edit Perms + {% trans "Edit Permissions" %}
{% csrf_token %} @@ -260,17 +296,67 @@
{% trans "Assign Permissions" %}
-
- {% for perm in permissions %} -
-
- - -
+
+
+ + + + + + + + + + + + {% regroup permissions by content_type as perm_list %} + {% for g in perm_list %} + + + + + + + + {% endfor %} + +
{% trans "Module" %}{% trans "View" %}{% trans "Add" %}{% trans "Edit" %}{% trans "Delete" %}
+
{{ g.grouper.name|capfirst }}
+
{{ g.grouper.app_label }}
+
+ {% for perm in g.list %} + {% if "view" in perm.codename %} +
+ +
+ {% endif %} + {% endfor %} +
+ {% for perm in g.list %} + {% if "add" in perm.codename %} +
+ +
+ {% endif %} + {% endfor %} +
+ {% for perm in g.list %} + {% if "change" in perm.codename %} +
+ +
+ {% endif %} + {% endfor %} +
+ {% for perm in g.list %} + {% if "delete" in perm.codename %} +
+ +
+ {% endif %} + {% endfor %} +
- {% endfor %}
{% trans "Permissions" %}
-
- {% for perm in permissions %} -
-
- - -
+
+
+ + + + + + + + + + + + {% regroup permissions by content_type as perm_list %} + {% for g in perm_list %} + + + + + + + + {% endfor %} + +
{% trans "Module" %}{% trans "View" %}{% trans "Add" %}{% trans "Edit" %}{% trans "Delete" %}
+
{{ g.grouper.name|capfirst }}
+
{{ g.grouper.app_label }}
+
+ {% for perm in g.list %} + {% if "view" in perm.codename %} +
+ +
+ {% endif %} + {% endfor %} +
+ {% for perm in g.list %} + {% if "add" in perm.codename %} +
+ +
+ {% endif %} + {% endfor %} +
+ {% for perm in g.list %} + {% if "change" in perm.codename %} +
+ +
+ {% endif %} + {% endfor %} +
+ {% for perm in g.list %} + {% if "delete" in perm.codename %} +
+ +
+ {% endif %} + {% endfor %} +
- {% endfor %}