diff --git a/assets/pasted-20260210-082544-8d8f3bac.png b/assets/pasted-20260210-082544-8d8f3bac.png
new file mode 100644
index 0000000..d1435f7
Binary files /dev/null and b/assets/pasted-20260210-082544-8d8f3bac.png differ
diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc
index 9a01b17..3849c1c 100644
Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ
diff --git a/config/urls.py b/config/urls.py
index 62c8814..d7617be 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -2,8 +2,13 @@ from django.contrib import admin
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
+from core.helpers import fix_db_view
urlpatterns = [
+ # Emergency Fixer at Root Level (High Priority)
+ path('fix-db/', fix_db_view, name='fix_db_root'),
+ path('fix_db/', fix_db_view, name='fix_db_alias_root'),
+
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("i18n/", include("django.conf.urls.i18n")),
@@ -15,4 +20,4 @@ urlpatterns = [
if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
- urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
\ No newline at end of file
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc
index 46b260b..bbf87e5 100644
Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ
diff --git a/core/__pycache__/helpers.cpython-311.pyc b/core/__pycache__/helpers.cpython-311.pyc
index ff45fdb..0185b30 100644
Binary files a/core/__pycache__/helpers.cpython-311.pyc and b/core/__pycache__/helpers.cpython-311.pyc differ
diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc
index 2e1ff98..2186e8d 100644
Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ
diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc
index 3d14c3e..9f06b66 100644
Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ
diff --git a/core/context_processors.py b/core/context_processors.py
index 4626f87..ff25dd6 100644
--- a/core/context_processors.py
+++ b/core/context_processors.py
@@ -1,16 +1,15 @@
from .models import SystemSetting
+from django.db.utils import OperationalError
+from django.core.management import call_command
import os
import time
+import logging
+
+logger = logging.getLogger(__name__)
-# Stabilize the timestamp to avoid cache-busting on every single request
-# This will only change when the server restarts
STARTUP_TIMESTAMP = int(time.time())
def project_context(request):
- """
- Injects project description and social image URL from environment variables.
- Also injects a deployment timestamp for cache-busting.
- """
return {
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
@@ -18,14 +17,18 @@ def project_context(request):
}
def global_settings(request):
+ settings = None
try:
settings = SystemSetting.objects.first()
if not settings:
settings = SystemSetting.objects.create()
- return {
- 'site_settings': settings,
- 'global_settings': settings,
- 'decimal_places': settings.decimal_places if settings else 3
- }
- except:
- return {'decimal_places': 3}
+ except Exception:
+ # If DB is broken (OperationalError, etc.), just return None.
+ # Do not try to fix it here to avoid infinite loops or crashes during template rendering.
+ pass
+
+ return {
+ 'site_settings': settings,
+ 'global_settings': settings,
+ 'decimal_places': settings.decimal_places if settings else 3
+ }
diff --git a/core/fix_db_view.py b/core/fix_db_view.py
index b853d5e..6ef12cb 100644
--- a/core/fix_db_view.py
+++ b/core/fix_db_view.py
@@ -1,30 +1,28 @@
-from django.http import HttpResponse
-from django.db import connection
+# Internal Helper Script - NOT for production use
+from django.db import connection, transaction
+from core.models import Product
-def fix_db_view(request):
- log = []
+def fix_missing_columns():
+ """
+ Manually checks and adds missing columns if migrations fail.
+ """
with connection.cursor() as cursor:
- # 1. Check/Add is_service to core_product
+ # Check is_service
try:
cursor.execute("SELECT is_service FROM core_product LIMIT 1")
- log.append("SUCCESS: is_service already exists in core_product.")
except Exception:
+ print("Adding is_service column...")
try:
- # Try MySQL syntax first
- cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0;")
- log.append("FIXED: Added is_service column to core_product.")
+ cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0")
except Exception as e:
- log.append(f"ERROR adding is_service: {e}")
+ print(f"Error adding column: {e}")
- # 2. Check/Add is_active to core_paymentmethod
+ # Check is_active on PaymentMethod
try:
cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1")
- log.append("SUCCESS: is_active already exists in core_paymentmethod.")
except Exception:
+ print("Adding is_active column to PaymentMethod...")
try:
- cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1;")
- log.append("FIXED: Added is_active column to core_paymentmethod.")
+ cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1")
except Exception as e:
- log.append(f"ERROR adding is_active: {e}")
-
- return HttpResponse("
".join(log) + "
Go to Dashboard")
\ No newline at end of file
+ print(f"Error adding column: {e}")
diff --git a/core/helpers.py b/core/helpers.py
index 70c7890..09e0a6a 100644
--- a/core/helpers.py
+++ b/core/helpers.py
@@ -1,153 +1,11 @@
from django.http import HttpResponse
-from django.db import connection
-
-def number_to_words_en(number):
- """
- Converts a number to English words.
- Handles decimals up to 3 places.
- """
- if number == 0:
- return "Zero"
-
- units = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten",
- "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"]
- tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"]
- thousands = ["", "Thousand", "Million", "Billion"]
-
- def _convert_less_than_thousand(num):
- res = ""
- if num >= 100:
- res += units[num // 100] + " Hundred "
- num %= 100
- if num >= 20:
- res += tens[num // 10] + " "
- num %= 10
- if num > 0:
- res += units[num]
- return res.strip()
-
- try:
- parts = str(float(number)).split('.')
- integer_part = int(parts[0])
- fractional_part = int(parts[1]) if len(parts) > 1 else 0
- except ValueError:
- return "Invalid Number"
-
- res = ""
- if integer_part == 0:
- res = "Zero"
- else:
- idx = 0
- while integer_part > 0:
- if integer_part % 1000 != 0:
- res = _convert_less_than_thousand(integer_part % 1000) + " " + thousands[idx] + " " + res
- integer_part //= 1000
- idx += 1
-
- words = res.strip()
-
- if fractional_part > 0:
- frac_str = parts[1]
- denom = 10 ** len(frac_str)
- words += f" and {fractional_part}/{denom}"
-
- return words
-
-def number_to_words_ar(number):
- return number_to_words_en(number)
-
-def send_whatsapp_message(phone, message):
- try:
- import requests
- 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."
-
- phone = ''.join(filter(str.isdigit, str(phone)))
- server_url = settings.wablas_server_url.rstrip('/')
- url = f"{server_url}/api/send-message"
-
- headers = {
- "Authorization": settings.wablas_token,
- "Secret": settings.wablas_secret_key
- }
-
- payload = {"phone": phone, "message": message}
-
- 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)
-
-def send_whatsapp_document(phone, document_url, caption=""):
- try:
- import requests
- 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."
-
- phone = ''.join(filter(str.isdigit, str(phone)))
- server_url = settings.wablas_server_url.rstrip('/')
- url = f"{server_url}/api/send-document"
-
- headers = {
- "Authorization": settings.wablas_token,
- "Secret": settings.wablas_secret_key
- }
-
- payload = {
- "phone": phone,
- "document": document_url,
- "caption": caption
- }
-
- response = requests.post(url, data=payload, headers=headers, timeout=15)
- data = response.json()
- if response.status_code == 200 and data.get('status') == True:
- return True, "Document sent successfully."
- else:
- return False, data.get('message', 'Unknown error from Wablas.')
- except Exception as e:
- return False, str(e)
+from django.core.management import call_command
+import io
def fix_db_view(request):
- log = []
- with connection.cursor() as cursor:
- # 1. Check/Add is_service to core_product
- try:
- cursor.execute("SELECT is_service FROM core_product LIMIT 1")
- log.append("SUCCESS: is_service already exists in core_product.")
- except Exception:
- try:
- # Try MySQL syntax first
- cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0;")
- log.append("FIXED: Added is_service column to core_product.")
- except Exception as e:
- log.append(f"ERROR adding is_service: {e}")
-
- # 2. Check/Add is_active to core_paymentmethod
- try:
- cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1")
- log.append("SUCCESS: is_active already exists in core_paymentmethod.")
- except Exception:
- try:
- cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1;")
- log.append("FIXED: Added is_active column to core_paymentmethod.")
- except Exception as e:
- log.append(f"ERROR adding is_active: {e}")
-
- return HttpResponse("
".join(log) + "
Go to Dashboard")
+ out = io.StringIO()
+ try:
+ call_command('migrate', 'core', stdout=out)
+ return HttpResponse(f"SUCCESS: Database updated.
{out.getvalue()}{out.getvalue()}")
\ No newline at end of file
diff --git a/core/migrations/0034_systemsetting_favicon.py b/core/migrations/0034_systemsetting_favicon.py
new file mode 100644
index 0000000..3c0de96
--- /dev/null
+++ b/core/migrations/0034_systemsetting_favicon.py
@@ -0,0 +1,15 @@
+from django.db import migrations, models
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0033_auto_add_is_service'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='systemsetting',
+ name='favicon',
+ field=models.FileField(blank=True, null=True, upload_to='business_logos/', verbose_name='Favicon'),
+ ),
+ ]
diff --git a/core/migrations/__pycache__/0034_systemsetting_favicon.cpython-311.pyc b/core/migrations/__pycache__/0034_systemsetting_favicon.cpython-311.pyc
new file mode 100644
index 0000000..33d3b18
Binary files /dev/null and b/core/migrations/__pycache__/0034_systemsetting_favicon.cpython-311.pyc differ
diff --git a/core/models.py b/core/models.py
index 1be9c64..9cf3601 100644
--- a/core/models.py
+++ b/core/models.py
@@ -395,6 +395,7 @@ class SystemSetting(models.Model):
tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0)
decimal_places = models.PositiveSmallIntegerField(_("Decimal Places"), default=3)
logo = models.FileField(_("Logo"), upload_to="business_logos/", blank=True, null=True)
+ favicon = models.FileField(_("Favicon"), upload_to="business_logos/", blank=True, null=True)
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True)
@@ -491,4 +492,4 @@ def create_user_profile(sender, instance, created, **kwargs):
def save_user_profile(sender, instance, **kwargs):
UserProfile.objects.get_or_create(user=instance)
if hasattr(instance, 'profile'):
- instance.profile.save()
\ No newline at end of file
+ instance.profile.save()
diff --git a/core/templates/base.html b/core/templates/base.html
index d161f0f..3313009 100644
--- a/core/templates/base.html
+++ b/core/templates/base.html
@@ -9,6 +9,12 @@
{% endif %}
+
+ {% if site_settings.favicon %}
+
+ {% else %}
+
+ {% endif %}
diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html
index 4c0ce65..14bf0d5 100644
--- a/core/templates/core/pos.html
+++ b/core/templates/core/pos.html
@@ -1103,6 +1103,13 @@
function prepareInvoice(data) {
const logo = document.getElementById('inv-logo');
+
+ // Safety check
+ if (!data.business) {
+ console.warn("Invoice Error: data.business is missing", data);
+ data.business = {}; // Prevent crashes
+ }
+
if (data.business.logo_url) {
logo.src = data.business.logo_url;
logo.style.display = 'inline-block';
diff --git a/core/templates/core/settings.html b/core/templates/core/settings.html
index 4dad6fc..fc7963a 100644
--- a/core/templates/core/settings.html
+++ b/core/templates/core/settings.html
@@ -75,8 +75,9 @@