deploy moti 222

This commit is contained in:
Flatlogic Bot 2026-02-10 08:35:25 +00:00
parent e6865dae2e
commit 46a59fc51c
17 changed files with 253 additions and 204 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -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)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -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
}

View File

@ -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("<br>".join(log) + "<br><br><a href='/'>Go to Dashboard</a>")
print(f"Error adding column: {e}")

View File

@ -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("<br>".join(log) + "<br><br><a href='/'>Go to Dashboard</a>")
out = io.StringIO()
try:
call_command('migrate', 'core', stdout=out)
return HttpResponse(f"SUCCESS: Database updated.<br><pre>{out.getvalue()}</pre><br><a href='/'>Go Home</a>")
except Exception as e:
return HttpResponse(f"ERROR: {e}<br><pre>{out.getvalue()}</pre>")

View File

@ -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'),
),
]

View File

@ -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()
instance.profile.save()

View File

@ -9,6 +9,12 @@
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
{% endif %}
{% if site_settings.favicon %}
<link rel="icon" type="image/png" href="{{ site_settings.favicon.url }}">
{% else %}
<link rel="icon" type="image/x-icon" href="{% static 'favicon.ico' %}">
{% endif %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

View File

@ -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';

View File

@ -75,8 +75,9 @@
<div class="card-body">
<form method="post" enctype="multipart/form-data" action="{% url 'settings' %}">
{% csrf_token %}
<input type="hidden" name="setting_type" value="profile">
<div class="row g-3">
<div class="col-md-12 text-center mb-3">
<div class="col-md-6 text-center mb-3">
<label class="form-label d-block fw-semibold">{% trans "Business Logo" %}</label>
{% if settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo" class="img-thumbnail mb-2" style="max-height: 100px;">
@ -87,6 +88,17 @@
{% endif %}
<input type="file" name="logo" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*">
</div>
<div class="col-md-6 text-center mb-3">
<label class="form-label d-block fw-semibold">{% trans "Favicon" %}</label>
{% if settings.favicon %}
<img src="{{ settings.favicon.url }}" alt="Favicon" class="img-thumbnail mb-2" style="max-height: 50px;">
{% else %}
<div class="bg-light border rounded d-inline-flex align-items-center justify-content-center mb-2" style="width: 50px; height: 50px;">
<i class="bi bi-browser-chrome text-muted fs-4"></i>
</div>
{% endif %}
<input type="file" name="favicon" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*">
</div>
<div class="col-md-12">
<label class="form-label fw-semibold">{% trans "Business Name" %}</label>
@ -572,6 +584,7 @@
<div class="card-body">
<form method="post" action="{% url 'settings' %}">
{% csrf_token %}
<input type="hidden" name="setting_type" value="whatsapp">
<div class="row g-3">
<div class="col-md-12">
<div class="form-check form-switch mb-3">
@ -774,7 +787,7 @@
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Save Device" %}</button>
<button type="button" class="btn btn-primary">{% trans "Save Device" %}</button>
</div>
</form>
</div>

View File

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