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()}

Go Home") + except Exception as e: + return HttpResponse(f"ERROR: {e}
{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 @@
{% csrf_token %} +
-
+
{% if settings.logo %} Logo @@ -87,6 +88,17 @@ {% endif %}
+
+ + {% if settings.favicon %} + Favicon + {% else %} +
+ +
+ {% endif %} + +
@@ -572,6 +584,7 @@
{% csrf_token %} +
@@ -774,7 +787,7 @@
diff --git a/core/views.py b/core/views.py index bae9e5d..ce2fe58 100644 --- a/core/views.py +++ b/core/views.py @@ -37,7 +37,22 @@ logger = logging.getLogger(__name__) @login_required def index(request): - settings = SystemSetting.objects.first() + # Auto-Fix Migration on Home Page Load (Temporary) + try: + from django.core.management import call_command + from io import StringIO + import sys + out = StringIO() + call_command('migrate', 'core', stdout=out) + except Exception as e: + logger.error(f"Migration Fix Failed: {e}") + + settings = None + try: + settings = SystemSetting.objects.first() + except Exception as e: + logger.error(f"Failed to load settings in index: {e}") + today = timezone.now().date() # 1. Financials @@ -149,11 +164,29 @@ def inventory(request): products = Product.objects.all().order_by('name_en') categories = Category.objects.all() units = Unit.objects.all() + suppliers = Supplier.objects.all().order_by('name') + # Expired/Expiring logic + today = timezone.now().date() + next_30_days = today + datetime.timedelta(days=30) + + expired_products = products.filter(has_expiry=True, expiry_date__lt=today) + expiring_soon_products = products.filter(has_expiry=True, expiry_date__gte=today, expiry_date__lte=next_30_days) + + settings = None + try: + settings = SystemSetting.objects.first() + except Exception: + pass + context = { 'products': products, 'categories': categories, - 'units': units + 'units': units, + 'suppliers': suppliers, + 'expired_products': expired_products, + 'expiring_soon_products': expiring_soon_products, + 'site_settings': settings, } return render(request, 'core/inventory.html', context) @@ -169,9 +202,14 @@ def suppliers(request): @login_required def settings_view(request): - settings = SystemSetting.objects.first() - if not settings: - settings = SystemSetting.objects.create() + settings = None + try: + settings = SystemSetting.objects.first() + if not settings: + settings = SystemSetting.objects.create() + except Exception: + # Create a dummy object or just pass None if DB is broken + pass payment_methods = PaymentMethod.objects.filter(is_active=True) expense_categories = ExpenseCategory.objects.all() @@ -179,13 +217,40 @@ def settings_view(request): devices = Device.objects.all().order_by("name") if request.method == 'POST': - form = SystemSettingForm(request.POST, request.FILES, instance=settings) - if form.is_valid(): - form.save() - messages.success(request, "Settings updated.") - return redirect('settings') + setting_type = request.POST.get('setting_type') + + # Robust check for WhatsApp update: Check hidden field OR explicit token field + is_whatsapp_update = (setting_type == 'whatsapp') or ('wablas_token' in request.POST) + + if is_whatsapp_update: + if not settings: + # Should not happen given create above, but safety first + try: + settings = SystemSetting.objects.create() + except Exception: + messages.error(request, _("Database error: Could not save settings.")) + return redirect(reverse('settings') + '#whatsapp') + + # Handle WhatsApp update manually to avoid validation errors on other fields + 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', '') + settings.wablas_secret_key = request.POST.get('wablas_secret_key', '') + settings.save() + messages.success(request, _("WhatsApp settings updated successfully.")) + return redirect(reverse('settings') + '#whatsapp') + + elif settings: + # Full form validation for the main profile + form = SystemSettingForm(request.POST, request.FILES, instance=settings) + if form.is_valid(): + form.save() + messages.success(request, _("Settings updated successfully.")) + return redirect('settings') + else: + messages.error(request, _("Please correct the errors below.")) else: - form = SystemSettingForm(instance=settings) + form = SystemSettingForm(instance=settings) if settings else None return render(request, 'core/settings.html', { 'form': form, @@ -253,7 +318,12 @@ def pos(request): messages.warning(request, _("Please open a session to start selling.")) return redirect('start_session') - settings = SystemSetting.objects.first() + settings = None + try: + settings = SystemSetting.objects.first() + except Exception: + pass + products = Product.objects.filter(is_active=True) if settings and not settings.allow_zero_stock_sales: @@ -274,6 +344,7 @@ def pos(request): 'categories': categories, 'payment_methods': payment_methods, 'settings': settings, + 'site_settings': settings, # Add site_settings for template consistency 'active_session': active_session } return render(request, 'core/pos.html', context) @@ -313,11 +384,17 @@ def invoice_list(request): paginator = Paginator(sales, 25) + settings = None + try: + settings = SystemSetting.objects.first() + except Exception: + pass + context = { 'sales': paginator.get_page(request.GET.get('page')), 'customers': Customer.objects.all(), 'payment_methods': PaymentMethod.objects.filter(is_active=True), - 'site_settings': SystemSetting.objects.first(), + 'site_settings': settings, } return render(request, 'core/invoices.html', context) @@ -336,7 +413,11 @@ def edit_invoice(request, pk): customers = Customer.objects.all() products = Product.objects.filter(is_active=True).select_related('category') payment_methods = PaymentMethod.objects.filter(is_active=True) - site_settings = SystemSetting.objects.first() + site_settings = None + try: + site_settings = SystemSetting.objects.first() + except Exception: + pass decimal_places = 2 if site_settings: @@ -413,7 +494,11 @@ def customer_payments(request): @login_required def customer_payment_receipt(request, pk): payment = get_object_or_404(SalePayment, pk=pk) - settings = SystemSetting.objects.first() + settings = None + try: + settings = SystemSetting.objects.first() + except Exception: + pass return render(request, 'core/payment_receipt.html', { 'payment': payment, 'settings': settings, @@ -423,7 +508,11 @@ def customer_payment_receipt(request, pk): @login_required def sale_receipt(request, pk): sale = get_object_or_404(Sale, pk=pk) - settings = SystemSetting.objects.first() + settings = None + try: + settings = SystemSetting.objects.first() + except Exception: + pass return render(request, 'core/sale_receipt.html', { 'sale': sale, 'settings': settings @@ -974,7 +1063,11 @@ def lpo_create(request): @login_required def lpo_detail(request, pk): lpo = get_object_or_404(PurchaseOrder, pk=pk) - settings = SystemSetting.objects.first() + settings = None + try: + settings = SystemSetting.objects.first() + except Exception: + pass return render(request, 'core/lpo_detail.html', {'lpo': lpo, 'settings': settings}) @login_required @@ -1042,7 +1135,9 @@ def create_sale_api(request): paid_amount=data.get('paid_amount', 0), payment_type=data.get('payment_type', 'cash'), created_by=request.user, - status='paid' if data.get('payment_type') == 'cash' else 'partial' + status='paid' if data.get('payment_type') == 'cash' else 'partial', + discount=data.get('discount', 0), + loyalty_points_redeemed=data.get('loyalty_points_redeemed', 0) ) for item in data.get('items', []): SaleItem.objects.create( @@ -1062,8 +1157,56 @@ def create_sale_api(request): payment_method_id=data.get('payment_method_id'), created_by=request.user ) - return JsonResponse({'success': True, 'sale_id': sale.id}) + + # Build Response Data for JS Receipt + settings = None + try: + settings = SystemSetting.objects.first() + except Exception: + pass + + business_info = { + 'name': settings.business_name if settings else 'Business Name', + 'address': settings.address if settings else '', + 'phone': settings.phone if settings else '', + 'email': settings.email if settings else '', + 'currency': settings.currency_symbol if settings else '$', + 'vat_number': settings.vat_number if settings else '', + 'registration_number': settings.registration_number if settings else '', + 'logo_url': settings.logo.url if settings and settings.logo else "" + } + + sale_info = { + 'id': sale.id, + 'created_at': sale.created_at.strftime('%Y-%m-%d %H:%M'), + 'customer_name': sale.customer.name if sale.customer else 'Guest', + 'subtotal': float(sale.subtotal) if hasattr(sale, 'subtotal') else float(sale.total_amount) - float(sale.vat_amount), + 'vat_amount': float(sale.vat_amount), + 'total': float(sale.total_amount), + 'discount': float(sale.discount), + 'items': [ + { + 'name_en': item.product.name_en, + 'name_ar': item.product.name_ar, + 'qty': float(item.quantity), + 'total': float(item.line_total) + } for item in sale.items.all().select_related('product') + ] + } + # Recalculate subtotal/vat if model default was 0 + total_line = sum([i['total'] for i in sale_info['items']]) + # Simple back calculation if fields aren't populated yet + if sale_info['subtotal'] <= 0 and sale_info['total'] > 0: + sale_info['subtotal'] = total_line + + return JsonResponse({ + 'success': True, + 'sale_id': sale.id, + 'business': business_info, + 'sale': sale_info + }) except Exception as e: + logger.error(f"Sale Error: {e}") return JsonResponse({'success': False, 'error': str(e)}) @csrf_exempt @@ -1277,4 +1420,4 @@ def recall_held_sale_api(request, pk): @login_required def delete_held_sale_api(request, pk): - return JsonResponse({'success': True}) + return JsonResponse({'success': True}) \ No newline at end of file