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.urls import include, path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from core.helpers import fix_db_view
urlpatterns = [ 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("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")), path("accounts/", include("django.contrib.auth.urls")),
path("i18n/", include("django.conf.urls.i18n")), path("i18n/", include("django.conf.urls.i18n")),
@ -15,4 +20,4 @@ urlpatterns = [
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 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 .models import SystemSetting
from django.db.utils import OperationalError
from django.core.management import call_command
import os import os
import time 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()) STARTUP_TIMESTAMP = int(time.time())
def project_context(request): def project_context(request):
"""
Injects project description and social image URL from environment variables.
Also injects a deployment timestamp for cache-busting.
"""
return { return {
"project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
@ -18,14 +17,18 @@ def project_context(request):
} }
def global_settings(request): def global_settings(request):
settings = None
try: try:
settings = SystemSetting.objects.first() settings = SystemSetting.objects.first()
if not settings: if not settings:
settings = SystemSetting.objects.create() settings = SystemSetting.objects.create()
return { except Exception:
'site_settings': settings, # If DB is broken (OperationalError, etc.), just return None.
'global_settings': settings, # Do not try to fix it here to avoid infinite loops or crashes during template rendering.
'decimal_places': settings.decimal_places if settings else 3 pass
}
except: return {
return {'decimal_places': 3} '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 # Internal Helper Script - NOT for production use
from django.db import connection from django.db import connection, transaction
from core.models import Product
def fix_db_view(request): def fix_missing_columns():
log = [] """
Manually checks and adds missing columns if migrations fail.
"""
with connection.cursor() as cursor: with connection.cursor() as cursor:
# 1. Check/Add is_service to core_product # Check is_service
try: try:
cursor.execute("SELECT is_service FROM core_product LIMIT 1") cursor.execute("SELECT is_service FROM core_product LIMIT 1")
log.append("SUCCESS: is_service already exists in core_product.")
except Exception: except Exception:
print("Adding is_service column...")
try: try:
# Try MySQL syntax first cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0")
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: 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: try:
cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1") cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1")
log.append("SUCCESS: is_active already exists in core_paymentmethod.")
except Exception: except Exception:
print("Adding is_active column to PaymentMethod...")
try: try:
cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1;") 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: except Exception as e:
log.append(f"ERROR adding is_active: {e}") print(f"Error adding column: {e}")
return HttpResponse("<br>".join(log) + "<br><br><a href='/'>Go to Dashboard</a>")

View File

@ -1,153 +1,11 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.db import connection from django.core.management import call_command
import io
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)
def fix_db_view(request): def fix_db_view(request):
log = [] out = io.StringIO()
with connection.cursor() as cursor: try:
# 1. Check/Add is_service to core_product call_command('migrate', 'core', stdout=out)
try: return HttpResponse(f"SUCCESS: Database updated.<br><pre>{out.getvalue()}</pre><br><a href='/'>Go Home</a>")
cursor.execute("SELECT is_service FROM core_product LIMIT 1") except Exception as e:
log.append("SUCCESS: is_service already exists in core_product.") return HttpResponse(f"ERROR: {e}<br><pre>{out.getvalue()}</pre>")
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>")

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) tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0)
decimal_places = models.PositiveSmallIntegerField(_("Decimal Places"), default=3) decimal_places = models.PositiveSmallIntegerField(_("Decimal Places"), default=3)
logo = models.FileField(_("Logo"), upload_to="business_logos/", blank=True, null=True) 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) vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
registration_number = models.CharField(_("Registration 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): def save_user_profile(sender, instance, **kwargs):
UserProfile.objects.get_or_create(user=instance) UserProfile.objects.get_or_create(user=instance)
if hasattr(instance, 'profile'): if hasattr(instance, 'profile'):
instance.profile.save() instance.profile.save()

View File

@ -9,6 +9,12 @@
<meta name="description" content="{{ project_description }}"> <meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}"> <meta property="og:description" content="{{ project_description }}">
{% endif %} {% 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.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

View File

@ -1103,6 +1103,13 @@
function prepareInvoice(data) { function prepareInvoice(data) {
const logo = document.getElementById('inv-logo'); 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) { if (data.business.logo_url) {
logo.src = data.business.logo_url; logo.src = data.business.logo_url;
logo.style.display = 'inline-block'; logo.style.display = 'inline-block';

View File

@ -75,8 +75,9 @@
<div class="card-body"> <div class="card-body">
<form method="post" enctype="multipart/form-data" action="{% url 'settings' %}"> <form method="post" enctype="multipart/form-data" action="{% url 'settings' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="setting_type" value="profile">
<div class="row g-3"> <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> <label class="form-label d-block fw-semibold">{% trans "Business Logo" %}</label>
{% if settings.logo %} {% if settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo" class="img-thumbnail mb-2" style="max-height: 100px;"> <img src="{{ settings.logo.url }}" alt="Logo" class="img-thumbnail mb-2" style="max-height: 100px;">
@ -87,6 +88,17 @@
{% endif %} {% endif %}
<input type="file" name="logo" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*"> <input type="file" name="logo" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*">
</div> </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"> <div class="col-md-12">
<label class="form-label fw-semibold">{% trans "Business Name" %}</label> <label class="form-label fw-semibold">{% trans "Business Name" %}</label>
@ -572,6 +584,7 @@
<div class="card-body"> <div class="card-body">
<form method="post" action="{% url 'settings' %}"> <form method="post" action="{% url 'settings' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="setting_type" value="whatsapp">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-12"> <div class="col-md-12">
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
@ -774,7 +787,7 @@
</div> </div>
<div class="modal-footer bg-light border-0"> <div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button> <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> </div>
</form> </form>
</div> </div>

View File

@ -37,7 +37,22 @@ logger = logging.getLogger(__name__)
@login_required @login_required
def index(request): 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() today = timezone.now().date()
# 1. Financials # 1. Financials
@ -149,11 +164,29 @@ def inventory(request):
products = Product.objects.all().order_by('name_en') products = Product.objects.all().order_by('name_en')
categories = Category.objects.all() categories = Category.objects.all()
units = Unit.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 = { context = {
'products': products, 'products': products,
'categories': categories, '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) return render(request, 'core/inventory.html', context)
@ -169,9 +202,14 @@ def suppliers(request):
@login_required @login_required
def settings_view(request): def settings_view(request):
settings = SystemSetting.objects.first() settings = None
if not settings: try:
settings = SystemSetting.objects.create() 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) payment_methods = PaymentMethod.objects.filter(is_active=True)
expense_categories = ExpenseCategory.objects.all() expense_categories = ExpenseCategory.objects.all()
@ -179,13 +217,40 @@ def settings_view(request):
devices = Device.objects.all().order_by("name") devices = Device.objects.all().order_by("name")
if request.method == 'POST': if request.method == 'POST':
form = SystemSettingForm(request.POST, request.FILES, instance=settings) setting_type = request.POST.get('setting_type')
if form.is_valid():
form.save() # Robust check for WhatsApp update: Check hidden field OR explicit token field
messages.success(request, "Settings updated.") is_whatsapp_update = (setting_type == 'whatsapp') or ('wablas_token' in request.POST)
return redirect('settings')
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: else:
form = SystemSettingForm(instance=settings) form = SystemSettingForm(instance=settings) if settings else None
return render(request, 'core/settings.html', { return render(request, 'core/settings.html', {
'form': form, 'form': form,
@ -253,7 +318,12 @@ def pos(request):
messages.warning(request, _("Please open a session to start selling.")) messages.warning(request, _("Please open a session to start selling."))
return redirect('start_session') 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) products = Product.objects.filter(is_active=True)
if settings and not settings.allow_zero_stock_sales: if settings and not settings.allow_zero_stock_sales:
@ -274,6 +344,7 @@ def pos(request):
'categories': categories, 'categories': categories,
'payment_methods': payment_methods, 'payment_methods': payment_methods,
'settings': settings, 'settings': settings,
'site_settings': settings, # Add site_settings for template consistency
'active_session': active_session 'active_session': active_session
} }
return render(request, 'core/pos.html', context) return render(request, 'core/pos.html', context)
@ -313,11 +384,17 @@ def invoice_list(request):
paginator = Paginator(sales, 25) paginator = Paginator(sales, 25)
settings = None
try:
settings = SystemSetting.objects.first()
except Exception:
pass
context = { context = {
'sales': paginator.get_page(request.GET.get('page')), 'sales': paginator.get_page(request.GET.get('page')),
'customers': Customer.objects.all(), 'customers': Customer.objects.all(),
'payment_methods': PaymentMethod.objects.filter(is_active=True), 'payment_methods': PaymentMethod.objects.filter(is_active=True),
'site_settings': SystemSetting.objects.first(), 'site_settings': settings,
} }
return render(request, 'core/invoices.html', context) return render(request, 'core/invoices.html', context)
@ -336,7 +413,11 @@ def edit_invoice(request, pk):
customers = Customer.objects.all() customers = Customer.objects.all()
products = Product.objects.filter(is_active=True).select_related('category') products = Product.objects.filter(is_active=True).select_related('category')
payment_methods = PaymentMethod.objects.filter(is_active=True) 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 decimal_places = 2
if site_settings: if site_settings:
@ -413,7 +494,11 @@ def customer_payments(request):
@login_required @login_required
def customer_payment_receipt(request, pk): def customer_payment_receipt(request, pk):
payment = get_object_or_404(SalePayment, pk=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', { return render(request, 'core/payment_receipt.html', {
'payment': payment, 'payment': payment,
'settings': settings, 'settings': settings,
@ -423,7 +508,11 @@ def customer_payment_receipt(request, pk):
@login_required @login_required
def sale_receipt(request, pk): def sale_receipt(request, pk):
sale = get_object_or_404(Sale, pk=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', { return render(request, 'core/sale_receipt.html', {
'sale': sale, 'sale': sale,
'settings': settings 'settings': settings
@ -974,7 +1063,11 @@ def lpo_create(request):
@login_required @login_required
def lpo_detail(request, pk): def lpo_detail(request, pk):
lpo = get_object_or_404(PurchaseOrder, pk=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}) return render(request, 'core/lpo_detail.html', {'lpo': lpo, 'settings': settings})
@login_required @login_required
@ -1042,7 +1135,9 @@ def create_sale_api(request):
paid_amount=data.get('paid_amount', 0), paid_amount=data.get('paid_amount', 0),
payment_type=data.get('payment_type', 'cash'), payment_type=data.get('payment_type', 'cash'),
created_by=request.user, 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', []): for item in data.get('items', []):
SaleItem.objects.create( SaleItem.objects.create(
@ -1062,8 +1157,56 @@ def create_sale_api(request):
payment_method_id=data.get('payment_method_id'), payment_method_id=data.get('payment_method_id'),
created_by=request.user 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: except Exception as e:
logger.error(f"Sale Error: {e}")
return JsonResponse({'success': False, 'error': str(e)}) return JsonResponse({'success': False, 'error': str(e)})
@csrf_exempt @csrf_exempt
@ -1277,4 +1420,4 @@ def recall_held_sale_api(request, pk):
@login_required @login_required
def delete_held_sale_api(request, pk): def delete_held_sale_api(request, pk):
return JsonResponse({'success': True}) return JsonResponse({'success': True})